Add scope delimiter option (#17)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32df035..d629306 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index bd44c6a..af0b0c6 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -69,6 +69,9 @@
/// 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;
@@ -98,13 +101,21 @@
///
/// [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.
@@ -148,7 +159,7 @@
};
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);
}
@@ -261,7 +272,7 @@
headers: headers, body: body);
var credentials = handleAccessTokenResponse(
- response, tokenEndpoint, startTime, _scopes);
+ response, tokenEndpoint, startTime, _scopes, _delimiter);
return new Client(
credentials,
identifier: this.identifier,
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index 4531818..a0cbbc3 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -26,6 +26,9 @@
/// 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;
@@ -71,16 +74,22 @@
/// [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.
///
@@ -190,7 +199,7 @@
"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);
@@ -202,7 +211,7 @@
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.
diff --git a/lib/src/handle_access_token_response.dart b/lib/src/handle_access_token_response.dart
index e0fb6f6..70fe560 100644
--- a/lib/src/handle_access_token_response.dart
+++ b/lib/src/handle_access_token_response.dart
@@ -22,11 +22,14 @@
///
/// 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) =>
@@ -78,7 +81,7 @@
}
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);
diff --git a/lib/src/resource_owner_password_grant.dart b/lib/src/resource_owner_password_grant.dart
index 5427ecc..9986ba7 100644
--- a/lib/src/resource_owner_password_grant.dart
+++ b/lib/src/resource_owner_password_grant.dart
@@ -28,6 +28,10 @@
/// 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,
@@ -36,7 +40,9 @@
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 = {
@@ -56,13 +62,13 @@
}
}
- 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);
}
diff --git a/pubspec.yaml b/pubspec.yaml
index a8a05c0..87d5a23 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: oauth2
-version: 1.0.2
+version: 1.1.0
author: Dart Team <misc@dartlang.org>
homepage: http://github.com/dart-lang/oauth2
description: >
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index e03a607..db04725 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -46,6 +46,24 @@
'&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');
diff --git a/test/credentials_test.dart b/test/credentials_test.dart
index cedea6b..0d4eff0 100644
--- a/test/credentials_test.dart
+++ b/test/credentials_test.dart
@@ -95,6 +95,27 @@
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',
diff --git a/test/handle_access_token_response_test.dart b/test/handle_access_token_response_test.dart
index fe2a338..50343ed 100644
--- a/test/handle_access_token_response_test.dart
+++ b/test/handle_access_token_response_test.dart
@@ -16,7 +16,7 @@
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', () {
@@ -186,5 +186,18 @@
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']));
+ });
});
}
diff --git a/test/resource_owner_password_grant_test.dart b/test/resource_owner_password_grant_test.dart
index 1d73a0f..e6d3f35 100644
--- a/test/resource_owner_password_grant_test.dart
+++ b/test/resource_owner_password_grant_test.dart
@@ -87,6 +87,21 @@
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');