Skip to content

Additional OAuth2 Authentication for Hosted Repositories #2588

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

Closed
wants to merge 10 commits into from
1 change: 1 addition & 0 deletions lib/oauth2
Submodule oauth2 added at a52e1d
122 changes: 122 additions & 0 deletions lib/src/auth_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) 2015, 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.

import 'dart:convert';
import 'package:meta/meta.dart';

/// [AuthConfig] is used for setting up parameters required
/// to establish an OAUTH2 authentitcation. This class will be used with
/// [AuthorizationCodeGrant] from `oath2` library.
class AuthConfig {
const AuthConfig({
@required this.authorizationEndpoint,
@required this.identifier,
@required this.scopes,
@required this.secret,
@required this.tokenEndpoint,
this.useIdToken = false,
@required this.redirectOnAuthorization,
});

/// The URL of the authorization server endpoint that's used to authorize the
/// credentials.
///
/// This may be `null`, indicating that the credentials can't be authenticated.
final Uri authorizationEndpoint;

/// The URL of the authorization server endpoint that's used to refresh the
/// credentials.
///
/// This may be `null`, indicating that the credentials can't be refreshed.
final Uri tokenEndpoint;

/// The specific permissions being requested from the authorization server.
///
/// The scope strings are specific to the authorization server and may be
/// found in its documentation.
final List<String> scopes;

/// OAUTH server secret
final String secret;

/// OAUTH server client identifier.
final String identifier;

/// Use Id Token instead of access token in Authorization header
final bool useIdToken;

/// Url to redirect on successful authorization
final String redirectOnAuthorization;

/// Loads a set of auth configuration from a JSON-serialized form.
///
/// Throws a [FormatException] if the JSON is incorrectly formatted.
factory AuthConfig.fromJson(String json) {
void validate(condition, message) {
if (condition) return;
throw FormatException('Failed to load credentials: $message.\n\n$json');
}

dynamic parsed;
try {
parsed = jsonDecode(json);
} on FormatException {
validate(false, 'invalid JSON');
}

validate(parsed is Map, 'was not a JSON map');

for (var stringField in [
'identifier',
'authorizationEndpoint',
'tokenEndpoint',
'secret',
'redirectOnAuthorization'
]) {
var value = parsed[stringField];
validate(parsed.containsKey(stringField),
'did not contain required field "$stringField"');
validate(value == null || value is String,
'field "$stringField" was not a string, was "$value"');
}

var mapScopes = parsed['scopes'];
validate(mapScopes == null || mapScopes is List,
'field "scopes" was not a list, was "$mapScopes"');

var mapTokenEndpoint = parsed['tokenEndpoint'];
if (mapTokenEndpoint != null) {
mapTokenEndpoint = Uri.parse(mapTokenEndpoint);
}

var mapAuthorizationEndpoint = parsed['authorizationEndpoint'];
if (mapAuthorizationEndpoint != null) {
mapAuthorizationEndpoint = Uri.parse(mapAuthorizationEndpoint);
}

return AuthConfig(
authorizationEndpoint: mapAuthorizationEndpoint,
tokenEndpoint: mapTokenEndpoint,
identifier: parsed['identifier'],
secret: parsed['secret'],
redirectOnAuthorization: parsed['redirectOnAuthorization'],
scopes: [for (dynamic scope in mapScopes) scope.toString()],
useIdToken: parsed['useIdToken'] == null
? false
: parsed['useIdToken'].toString().toLowerCase() == 'true');
}

/// Map representation of [AuthConfig].
Map<String, dynamic> toMap() {
return <String, dynamic>{
'authorizationEndpoint': authorizationEndpoint.path,
'tokenEndpoint': tokenEndpoint.path,
'identifier': identifier,
'secret': secret,
'redirectOnAuthorization': redirectOnAuthorization,
'scopes': scopes,
'useIdToken': useIdToken
};
}
}
19 changes: 15 additions & 4 deletions lib/src/command/lish.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class LishCommand extends PubCommand {
/// Whether the publish requires confirmation.
bool get force => argResults['force'];

/// Optional API key for package server to which to upload this package.
bool get isHosted => argResults['server'] != null;

LishCommand() {
argParser.addFlag('dry-run',
abbr: 'n',
Expand All @@ -61,7 +64,7 @@ class LishCommand extends PubCommand {
negatable: false,
help: 'Publish without confirmation if there are no errors.');
argParser.addOption('server',
help: 'The package server to which to upload this package.');
abbr: 's', help: 'The package server to which to upload this package.');
}

Future _publish(List<int> packageBytes) async {
Expand All @@ -72,6 +75,7 @@ class LishCommand extends PubCommand {
// TODO(nweiz): Cloud Storage can provide an XML-formatted error. We
// should report that error and exit.
var newUri = server.resolve('/api/packages/versions/new');

var response = await client.get(newUri, headers: pubApiHeaders);
var parameters = parseJsonResponse(response);

Expand All @@ -90,14 +94,15 @@ class LishCommand extends PubCommand {
request.followRedirects = false;
request.files.add(http.MultipartFile.fromBytes('file', packageBytes,
filename: 'package.tar.gz'));

var postResponse =
await http.Response.fromStream(await client.send(request));

var location = postResponse.headers['location'];
if (location == null) throw PubHttpException(postResponse);
handleJsonSuccess(await client.get(location, headers: pubApiHeaders));
});
});
}, hostedURLName: isHosted ? server.host : null);
} on PubHttpException catch (error) {
var url = error.response.request.url;
if (url == cloudStorageUrl) {
Expand Down Expand Up @@ -162,8 +167,14 @@ class LishCommand extends PubCommand {
final warnings = <String>[];
final errors = <String>[];

await Validator.runAll(entrypoint, packageSize, server.toString(),
hints: hints, warnings: warnings, errors: errors);
await Validator.runAll(
entrypoint,
packageSize,
server.toString(),
hints: hints,
warnings: warnings,
errors: errors,
);

if (errors.isNotEmpty) {
log.error('Sorry, your package is missing '
Expand Down
2 changes: 1 addition & 1 deletion lib/src/command/logout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ class LogoutCommand extends PubCommand {

@override
Future run() async {
oauth2.logout(cache);
oauth2.logout(cache, null);
}
}
1 change: 1 addition & 0 deletions lib/src/null_safety_analysis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'dart:async';

import 'package:analyzer/dart/analysis/context_builder.dart';
import 'package:analyzer/dart/analysis/context_locator.dart';
// ignore: unused_import
import 'package:analyzer/dart/ast/token.dart';
import 'package:cli_util/cli_util.dart';
import 'package:source_span/source_span.dart';
Expand Down
Loading