Skip to content

Third party hosted pub registry authentication #3007

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 83 commits into from
Sep 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
218d114
wip: Prepare authentication proposal
themisir May 18, 2021
da1549f
doc: Add details about 'realm' parameter
themisir May 19, 2021
becf34f
Update authentication-proposal.md
themisir May 19, 2021
0999292
Merge branch 'dart-lang:master' into master
themisir May 19, 2021
ac8b70f
Implement CredentialStore and Credential for bearer method
themisir May 19, 2021
fc12fa8
Login and logout commands with hosted pub repo
themisir May 19, 2021
5a8d6f3
formatting: remove space
themisir May 19, 2021
427a7d9
Rename add/removeCredentials to add/removeServer
themisir May 19, 2021
fd45e4a
Allow wrapping http clients
themisir May 20, 2021
7955153
Implemented AuthenticationClient
themisir May 20, 2021
719005d
Authentication client added to stack of other clients
themisir May 20, 2021
383122d
Throw usage exception for more than one argument
themisir May 20, 2021
15e6238
Implemented publishing with saved credential support
themisir May 21, 2021
402154d
Modified authentication client
themisir May 21, 2021
bf675a3
Add comment documentation to AuthenticationClient
themisir May 21, 2021
13f0fc0
Rename to/fromMap to to/fromJson
themisir May 21, 2021
8e7fd96
Login / logout now uses option parameter instead of argument
themisir May 21, 2021
50a6b5f
Authentication now needs to be used explicitly when needed
themisir May 21, 2021
84a6c95
Update list command to use authenticated client
themisir May 21, 2021
8547a32
Change access modifier for authenticationType to @protected
themisir May 21, 2021
7896061
Add docs
themisir May 21, 2021
b2194de
Added file headers
themisir May 21, 2021
6aeaec1
Update lib/src/source/hosted.dart
themisir May 25, 2021
c917ec3
Rename server key to serverBaseUrl
themisir May 25, 2021
8d91007
Removed alsoMatches argument
themisir May 25, 2021
d6e0254
Rename _AuthenticationClient to _AuthenticatedClient
themisir May 26, 2021
094ad56
Update credential Json serialization
themisir May 26, 2021
4f652ff
Fixed credential server base url matching
themisir May 26, 2021
e937a37
Rename some "key"s to "serverBaseUrl"
themisir May 28, 2021
f800eef
Updated proposal document
themisir May 28, 2021
cac0fee
Modified http.dart
themisir May 28, 2021
be1fee1
Revert "Modified http.dart"
themisir May 28, 2021
2ad3ebd
Merge branch 'dart-lang:master' into master
themisir May 28, 2021
7286e4b
Modified error handling of withAuthenticatedClient
themisir May 29, 2021
3e5a3e2
Integration tests for login with server command
themisir May 29, 2021
1308b4a
Integration tests for logout server command
themisir May 29, 2021
4eea298
Modified JSON (de)serialization
themisir Jun 1, 2021
4ee285a
Annotate BearerCredential with @sealed
themisir Jun 1, 2021
5d57abb
doc, refac: Minor changes
themisir Jun 1, 2021
1cc1bd8
Modified tokens.json format on auth spec doc
themisir Jun 1, 2021
dbedbaa
Modified authentication modelling scheme
themisir Jun 1, 2021
ec703f6
Refactoring credential_store.dart
themisir Jun 1, 2021
5b44966
Merge remote-tracking branch 'upstream/master'
themisir Jun 5, 2021
f16fbbe
Request authenticated client using server base url instead of full re…
themisir Jun 5, 2021
7dcfd97
Exact URL matching for finding schemas
themisir Jun 5, 2021
d6399de
Modify _normalizeUrl method
themisir Jun 5, 2021
72973fc
URL normalization, login and logout commands are modified
themisir Jun 20, 2021
99bccd8
Modified scheme URL matching
themisir Jun 20, 2021
951e5c3
Remove credential and auth schema abstractions and replace them with …
themisir Aug 19, 2021
42fcf2b
Update tests for abstraction changes
themisir Aug 19, 2021
35d8896
Merge branch 'pr/themisir/2'
themisir Aug 19, 2021
030c6f9
Sorted directives
themisir Aug 19, 2021
946b547
Normalize token.url using validateAndNormalizeHostedUrl
themisir Aug 19, 2021
e602167
Modified removeMatchingTokens implementation
themisir Aug 30, 2021
0a37f19
pub login and logout commands reverted back
themisir Aug 30, 2021
73b921e
Updated constructor signature for Token class
themisir Aug 30, 2021
d431c08
Implemented token .. commands
themisir Aug 30, 2021
d2f4115
Removed authentication-proposal.md
themisir Aug 30, 2021
f70e766
Remove tokens.json on load error
themisir Aug 30, 2021
17400b0
Updated tests and fixed minor bugs
themisir Aug 30, 2021
38ca11c
Update helptext.txt golden
themisir Aug 30, 2021
ded22ce
Log token removal message in command body
themisir Aug 31, 2021
19ec461
Remove duplicate tokens when adding a new one
themisir Aug 31, 2021
fc0af7e
Print information when 401 and 403 received
themisir Aug 31, 2021
92352ed
Break out of loop after finding first duplicate
themisir Aug 31, 2021
0bfc2d0
Added or updated license headers
themisir Aug 31, 2021
b34dacc
Update lib/src/io.dart
themisir Sep 2, 2021
a27e89f
Change argument type of removeMatchingTokens from String to Uri
themisir Sep 2, 2021
d4ca151
Implement confirm using stdinPrompt
themisir Sep 2, 2021
b8a779e
Update messages for token commands
themisir Sep 2, 2021
3ca7d0f
Merge branch 'dart-lang:master' into master
themisir Sep 2, 2021
2e9105c
Remaining requested changes implemented
themisir Sep 2, 2021
344cba6
Merge branch 'master' of github.com:TheMisir/pub
themisir Sep 2, 2021
e92b69e
Updated token tests according to recent changes
themisir Sep 2, 2021
98549b2
Update helptext.txt golden
themisir Sep 2, 2021
5ed7e03
Do not expose oauth2 creds to 3rd party servers
themisir Sep 10, 2021
bebc6d2
Throw DataException when no token found for token remove
themisir Sep 10, 2021
cab0343
Merge branch 'dart-lang:master' into master
themisir Sep 10, 2021
82e5038
Persist unknown fields on unmodified token entries
themisir Sep 10, 2021
086053d
Merge branch 'master' into master
themisir Sep 13, 2021
69f8f5b
Allow mock servers to use oauth2 client
themisir Sep 13, 2021
7f3fa89
Truncate server message
themisir Sep 13, 2021
299a4f3
Minor refactoring for Credential and TokenStore
themisir Sep 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions lib/src/authentication/client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: import_of_legacy_library_into_null_safe

import 'dart:io';

import 'package:http/http.dart' as http;

import '../http.dart';
import '../log.dart' as log;
import '../system_cache.dart';
import 'credential.dart';

/// This client authenticates requests by injecting `Authentication` header to
/// requests.
///
/// Requests to URLs not under [serverBaseUrl] will not be authenticated.
class _AuthenticatedClient extends http.BaseClient {
_AuthenticatedClient(this._inner, this.credential);

final http.BaseClient _inner;

/// Authentication scheme that could be used for authenticating requests.
final Credential credential;

@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
// Let's last time make sure that, we're allowed to use credential for this
// request.
//
// This check ensures that this client will only authenticate requests sent
// to given serverBaseUrl. Otherwise credential leaks might ocurr when
// archive_url hosted on 3rd party server that should not receive
// credentials of the first party.
if (credential.canAuthenticate(request.url.toString())) {
request.headers[HttpHeaders.authorizationHeader] =
await credential.getAuthorizationHeaderValue();
}
return _inner.send(request);
}

@override
void close() => _inner.close();
}

/// Invoke [fn] with a [http.Client] capable of authenticating against
/// [hostedUrl].
///
/// Importantly, requests to URLs not under [hostedUrl] will not be
/// authenticated.
Future<T> withAuthenticatedClient<T>(
SystemCache systemCache,
Uri hostedUrl,
Future<T> Function(http.Client) fn,
) async {
final credential = systemCache.tokenStore.findCredential(hostedUrl);
final http.Client client = credential == null
? httpClient
: _AuthenticatedClient(httpClient, credential);

try {
return await fn(client);
} on PubHttpException catch (error) {
if (error.response?.statusCode == 401 ||
error.response?.statusCode == 403) {
// TODO(themisir): Do we need to match error.response.request.url with
// the hostedUrl? Or at least we might need to log request.url to give
// user additional insights on what's happening.

String? serverMessage;

try {
final wwwAuthenticateHeaderValue =
error.response.headers[HttpHeaders.wwwAuthenticateHeader];
if (wwwAuthenticateHeaderValue != null) {
final parsedValue = HeaderValue.parse(wwwAuthenticateHeaderValue,
parameterSeparator: ',');
if (parsedValue.parameters['realm'] == 'pub') {
serverMessage = parsedValue.parameters['message'];
}
}
} catch (_) {
// Ignore errors might be caused when parsing invalid header values
}

if (error.response.statusCode == 401) {
if (systemCache.tokenStore.removeCredential(hostedUrl)) {
log.warning('Invalid token for $hostedUrl deleted.');
}

log.error(
'Authentication requested by hosted server at: $hostedUrl\n'
'You can use the following command to add token for the server:\n'
'\n pub token add $hostedUrl\n',
);
}
if (error.response.statusCode == 403) {
log.error(
'Insufficient permissions to the resource in hosted server at: '
'$hostedUrl\n'
'You can use the following command to update token for the server:\n'
'\n pub token add $hostedUrl\n',
);
}

if (serverMessage?.isNotEmpty == true) {
// Only allow printable ASCII, map anything else to whitespace, take
// at-most 1024 characters.
final truncatedMessage = String.fromCharCodes(serverMessage!.runes
.map((r) => 32 >= r && r <= 127 ? r : 32)
.take(1024));

log.error(truncatedMessage);
}
}
rethrow;
}
}
109 changes: 109 additions & 0 deletions lib/src/authentication/credential.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// ignore_for_file: import_of_legacy_library_into_null_safe

import '../exceptions.dart';
import '../source/hosted.dart';

/// Token is a structure for storing authentication credentials for third-party
/// pub registries. A token holds registry [url], credential [kind] and [token]
/// itself.
///
/// Token could be serialized into and from JSON format structured like
/// this:
///
/// ```json
/// {
/// "url": "https://example.com/",
/// "token": "gjrjo7Tm2F0u64cTsECDq4jBNZYhco"
/// }
/// ```
class Credential {
/// Internal constructor that's only used by [fromJson].
Credential._internal({
required this.url,
required this.token,
required this.unknownFields,
});

/// Create a new [Credential].
Credential.token(this.url, this.token)
: unknownFields = const <String, dynamic>{};

/// Deserialize [json] into [Credential] type.
///
/// Throws [FormatException] if [json] is not a valid [Credential].
factory Credential.fromJson(Map<String, dynamic> json) {
if (json['url'] is! String) {
throw FormatException('Url is not provided for the credential');
}

final hostedUrl = validateAndNormalizeHostedUrl(json['url'] as String);

const knownKeys = {'url', 'token'};
final unknownFields = Map.fromEntries(
json.entries.where((kv) => !knownKeys.contains(kv.key)));

return Credential._internal(
url: hostedUrl,
token: json['token'] is String ? json['token'] as String : null,
unknownFields: unknownFields,
);
}

/// Server url which this token authenticates.
final Uri url;

/// Authentication token value
final String? token;

/// Unknown fields found in tokens.json. The fields might be created by the
/// future version of pub tool. We don't want to override them when using the
/// old SDK.
final Map<String, dynamic> unknownFields;

/// Serializes [Credential] into json format.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'url': url.toString(),
if (token != null) 'token': token,
...unknownFields,
};
}

/// Returns future that resolves "Authorization" header value used for
/// authenticating.
///
/// Throws [DataException] if credential is not valid.
// This method returns future to make sure in future we could use the
// [Credential] interface for OAuth2.0 authentication too - which requires
// token rotation (refresh) that's async job.
Future<String> getAuthorizationHeaderValue() {
if (!isValid()) {
throw DataException(
'Saved credential for $url pub repository is not supported by current '
'version of Dart SDK.',
);
Comment on lines +85 to +88
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this a format exception when loading. Then it'll get logged as a warning every time the user uses an old pub version with a newer config file.

Hmm, I can see that being less smart, but I also don't like dragging around nullability.

@sigurdm any thoughts here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is current saving mechanism will remove entries that doesn't contains 'token' property because it'll not be able to deserialize it. So they'll not added to the credential list and when saving those entries will be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to encapsulate json serialization part to Credential class itself to reduce code duplication for serialization & validation, otherwise the code would look like much confusing I think.

}

return Future.value('Bearer $token');
}

/// Returns whether or not given [url] could be authenticated using this
/// credential.
bool canAuthenticate(String url) {
return _normalizeUrl(url).startsWith(_normalizeUrl(this.url.toString()));
}

/// Returns boolean indicates whether or not the credentials is valid.
///
/// This method might return `false` when a `tokens.json` file created by
/// future SDK used by pub tool from old SDK.
bool isValid() => token != null;

static String _normalizeUrl(String url) {
return (url.endsWith('/') ? url : '$url/').toLowerCase();
}
}
Loading