Skip to content
This repository was archived by the owner on Aug 26, 2024. It is now read-only.

Add scope delimiter option #17

Merged
merged 18 commits into from
Jun 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 1.1.0

* Add a `delimiter` parameter to `new AuthorizationCodeGrant()`, `new
Credentials()`, and `resourceOwnerPasswordGrant()`. This controls the
delimiter between scopes, which some authorization servers require to be
different values than the specified `' '`.

# 1.0.2

* Fix all strong-mode warnings.
Expand Down
19 changes: 15 additions & 4 deletions lib/src/authorization_code_grant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ class AuthorizationCodeGrant {
/// Whether to use HTTP Basic authentication for authorizing the client.
final bool _basicAuth;

/// A [String] used to separate scopes; defaults to `" "`.
String _delimiter;

/// The HTTP client used to make HTTP requests.
http.Client _httpClient;

Expand Down Expand Up @@ -98,13 +101,21 @@ class AuthorizationCodeGrant {
///
/// [httpClient] is used for all HTTP requests made by this grant, as well as
/// those of the [Client] is constructs.
///
/// The scope strings will be separated by the provided [delimiter]. This
/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
/// use non-standard delimiters.
AuthorizationCodeGrant(
this.identifier,
this.authorizationEndpoint,
this.tokenEndpoint,
{this.secret, bool basicAuth: true, http.Client httpClient})
{this.secret,
String delimiter,
bool basicAuth: true,
http.Client httpClient})
: _basicAuth = basicAuth,
_httpClient = httpClient == null ? new http.Client() : httpClient;
_httpClient = httpClient == null ? new http.Client() : httpClient,
_delimiter = delimiter ?? ' ';

/// Returns the URL to which the resource owner should be redirected to
/// authorize this client.
Expand Down Expand Up @@ -148,7 +159,7 @@ class AuthorizationCodeGrant {
};

if (state != null) parameters['state'] = state;
if (!scopes.isEmpty) parameters['scope'] = scopes.join(' ');
if (!scopes.isEmpty) parameters['scope'] = scopes.join(_delimiter);

return addQueryParameters(this.authorizationEndpoint, parameters);
}
Expand Down Expand Up @@ -261,7 +272,7 @@ class AuthorizationCodeGrant {
headers: headers, body: body);

var credentials = handleAccessTokenResponse(
response, tokenEndpoint, startTime, _scopes);
response, tokenEndpoint, startTime, _scopes, _delimiter);
return new Client(
credentials,
identifier: this.identifier,
Expand Down
17 changes: 13 additions & 4 deletions lib/src/credentials.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import 'utils.dart';
/// Note that a given set of credentials can only be refreshed once, so be sure
/// to save the refreshed credentials for future use.
class Credentials {
/// A [String] used to separate scopes; defaults to `" "`.
String _delimiter;

/// The token that is sent to the resource server to prove the authorization
/// of a client.
final String accessToken;
Expand Down Expand Up @@ -71,16 +74,22 @@ class Credentials {
/// [Client.credentials] after a [Client] is created by
/// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized
/// form via [Credentials.fromJson].
///
/// The scope strings will be separated by the provided [delimiter]. This
/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
/// use non-standard delimiters.
Credentials(
this.accessToken,
{this.refreshToken,
this.tokenEndpoint,
Iterable<String> scopes,
this.expiration})
this.expiration,
String delimiter})
: scopes = new UnmodifiableListView(
// Explicitly type-annotate the list literal to work around
// sdk#24202.
scopes == null ? <String>[] : scopes.toList());
scopes == null ? <String>[] : scopes.toList()),
_delimiter = delimiter ?? ' ';

/// Loads a set of credentials from a JSON-serialized form.
///
Expand Down Expand Up @@ -190,7 +199,7 @@ class Credentials {
"grant_type": "refresh_token",
"refresh_token": refreshToken
};
if (!scopes.isEmpty) body["scope"] = scopes.join(' ');
if (!scopes.isEmpty) body["scope"] = scopes.join(_delimiter);

if (basicAuth && secret != null) {
headers["Authorization"] = basicAuthHeader(identifier, secret);
Expand All @@ -202,7 +211,7 @@ class Credentials {
var response = await httpClient.post(tokenEndpoint,
headers: headers, body: body);
var credentials = await handleAccessTokenResponse(
response, tokenEndpoint, startTime, scopes);
response, tokenEndpoint, startTime, scopes, _delimiter);

// The authorization server may issue a new refresh token. If it doesn't,
// we should re-use the one we already have.
Expand Down
7 changes: 5 additions & 2 deletions lib/src/handle_access_token_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ const _expirationGrace = const Duration(seconds: 10);
///
/// This response format is common across several different components of the
/// OAuth2 flow.
///
/// The scope strings will be separated by the provided [delimiter].
Credentials handleAccessTokenResponse(
http.Response response,
Uri tokenEndpoint,
DateTime startTime,
List<String> scopes) {
List<String> scopes,
String delimiter) {
if (response.statusCode != 200) _handleErrorResponse(response, tokenEndpoint);

validate(condition, message) =>
Expand Down Expand Up @@ -78,7 +81,7 @@ Credentials handleAccessTokenResponse(
}

var scope = parameters['scope'] as String;
if (scope != null) scopes = scope.split(" ");
if (scope != null) scopes = scope.split(delimiter);

var expiration = expiresIn == null ? null :
startTime.add(new Duration(seconds: expiresIn) - _expirationGrace);
Expand Down
12 changes: 9 additions & 3 deletions lib/src/resource_owner_password_grant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import 'utils.dart';
/// may not be granted access to every scope you request; you may check the
/// [Credentials.scopes] field of [Client.credentials] to see which scopes you
/// were granted.
///
/// The scope strings will be separated by the provided [delimiter]. This
/// defaults to `" "`, the OAuth2 standard, but some APIs (such as Facebook's)
/// use non-standard delimiters.
Future<Client> resourceOwnerPasswordGrant(
Uri authorizationEndpoint,
String username,
Expand All @@ -36,7 +40,9 @@ Future<Client> resourceOwnerPasswordGrant(
String secret,
Iterable<String> scopes,
bool basicAuth: true,
http.Client httpClient}) async {
http.Client httpClient,
String delimiter}) async {
delimiter ??= ' ';
var startTime = new DateTime.now();

var body = {
Expand All @@ -56,13 +62,13 @@ Future<Client> resourceOwnerPasswordGrant(
}
}

if (scopes != null && !scopes.isEmpty) body['scope'] = scopes.join(' ');
if (scopes != null && !scopes.isEmpty) body['scope'] = scopes.join(delimiter);

if (httpClient == null) httpClient = new http.Client();
var response = await httpClient.post(authorizationEndpoint,
headers: headers, body: body);

var credentials = await handleAccessTokenResponse(
response, authorizationEndpoint, startTime, scopes);
response, authorizationEndpoint, startTime, scopes, delimiter);
return new Client(credentials, identifier: identifier, secret: secret);
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: oauth2
version: 1.0.2
version: 1.1.0
author: Dart Team <[email protected]>
homepage: http://github.com/dart-lang/oauth2
description: >
Expand Down
18 changes: 18 additions & 0 deletions test/authorization_code_grant_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ void main() {
'&scope=scope+other%2Fscope'));
});

test('separates scopes with the correct delimiter', () {
var grant = new oauth2.AuthorizationCodeGrant(
'identifier',
Uri.parse('https://example.com/authorization'),
Uri.parse('https://example.com/token'),
secret: 'secret',
httpClient: client,
delimiter: '_');
var authorizationUrl = grant.getAuthorizationUrl(
redirectUrl, scopes: ['scope', 'other/scope']);
expect(authorizationUrl.toString(),
equals('https://example.com/authorization'
'?response_type=code'
'&client_id=identifier'
'&redirect_uri=http%3A%2F%2Fexample.com%2Fredirect'
'&scope=scope_other%2Fscope'));
});

test('builds the correct URL with state', () {
var authorizationUrl = grant.getAuthorizationUrl(
redirectUrl, state: 'state');
Expand Down
21 changes: 21 additions & 0 deletions test/credentials_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ void main() {
expect(credentials.refreshToken, equals('new refresh token'));
});

test('sets proper scope string when using custom delimiter', () async {
var credentials = new oauth2.Credentials(
'access token',
refreshToken: 'refresh token',
tokenEndpoint: tokenEndpoint,
scopes: ['scope1', 'scope2'],
delimiter: ',');
httpClient.expectRequest((http.Request request) {
expect(request.bodyFields['scope'], equals('scope1,scope2'));
return new Future.value(new http.Response(JSON.encode({
'access_token': 'new access token',
'token_type': 'bearer',
'refresh_token': 'new refresh token'
}), 200, headers: {'content-type': 'application/json'}));
});
await credentials.refresh(
identifier: 'idëntīfier',
secret: 'sëcret',
httpClient: httpClient);
});

test("can refresh without a client secret", () async {
var credentials = new oauth2.Credentials(
'access token',
Expand Down
15 changes: 14 additions & 1 deletion test/handle_access_token_response_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final Uri tokenEndpoint = Uri.parse("https://example.com/token");
final DateTime startTime = new DateTime.now();

oauth2.Credentials handle(http.Response response) =>
handleAccessTokenResponse(response, tokenEndpoint, startTime, ["scope"]);
handleAccessTokenResponse(response, tokenEndpoint, startTime, ["scope"], ' ');

void main() {
group('an error response', () {
Expand Down Expand Up @@ -186,5 +186,18 @@ void main() {
var credentials = handleSuccess(scope: "scope1 scope2");
expect(credentials.scopes, equals(["scope1", "scope2"]));
});

test('with a custom scope delimiter sets the scopes', () {
var response = new http.Response(JSON.encode({
'access_token': 'access token',
'token_type': 'bearer',
'expires_in': null,
'refresh_token': null,
'scope': 'scope1,scope2'
}), 200, headers: {'content-type': 'application/json'});
var credentials = handleAccessTokenResponse(
response, tokenEndpoint, startTime, ['scope'], ',');
expect(credentials.scopes, equals(['scope1', 'scope2']));
});
});
}
15 changes: 15 additions & 0 deletions test/resource_owner_password_grant_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ void main() {
expect(client.credentials.accessToken, equals('2YotnFZFEjr1zCsicMWpAA'));
});

test('builds correct request using scope with custom delimiter', () async {
expectClient.expectRequest((request) async {
expect(request.bodyFields['grant_type'], equals('password'));
expect(request.bodyFields['username'], equals('username'));
expect(request.bodyFields['password'], equals('userpass'));
expect(request.bodyFields['scope'], equals('one,two'));
return new http.Response(success, 200,
headers: {'content-type': 'application/json'});
});

await oauth2.resourceOwnerPasswordGrant(
authEndpoint, 'username', 'userpass',
scopes: ['one', 'two'], httpClient: expectClient, delimiter: ',');
});

test('merges with existing query parameters', () async {
var authEndpoint = Uri.parse('https://example.com?query=value');

Expand Down