Code review changes
BUG=
Review URL: https://codereview.chromium.org//1304363004 .
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc869c9..686b6ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,35 @@
+# 1.0.0
+
+## Breaking changes
+
+* Requests that use client authentication, such as the
+ `AuthorizationCodeGrant`'s access token request and `Credentials`' refresh
+ request, now use HTTP Basic authentication by default. This form of
+ authentication is strongly recommended by the OAuth 2.0 spec. The new
+ `basicAuth` parameter may be set to `false` to force form-based authentication
+ for servers that require it.
+
+* `new AuthorizationCodeGrant()` now takes `secret` as an optional named
+ argument rather than a required argument. This matches the OAuth 2.0 spec,
+ which says that a client secret is only required for confidential clients.
+
+* `new Client()` and `Credentials.refresh()` now take both `identifier` and
+ `secret` as optional named arguments rather than required arguments. This
+ matches the OAuth 2.0 spec, which says that the server may choose not to
+ require client authentication for some flows.
+
+* `new Credentials()` now takes named arguments rather than optional positional
+ arguments.
+
+## Non-breaking changes
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` and
+ `new Credentials()` and the `newScopes` argument to `Credentials.refresh` now
+ take an `Iterable` rather than just a `List`.
+
+* The `scopes` argument to `AuthorizationCodeGrant.getAuthorizationUrl()` now
+ defaults to `null` rather than `const []`.
+
# 0.9.3
* Update the `http` dependency.
diff --git a/README.md b/README.md
index 23be5ff..7e390cd 100644
--- a/README.md
+++ b/README.md
@@ -64,13 +64,15 @@
if (exists) {
var credentials = new oauth2.Credentials.fromJson(
await credentialsFile.readAsString());
- return new oauth2.Client(identifier, secret, credentials);
+ return new oauth2.Client(credentials,
+ identifier: identifier, secret: secret);
}
// If we don't have OAuth2 credentials yet, we need to get the resource owner
// to authorize us. We're assuming here that we're a command-line application.
var grant = new oauth2.AuthorizationCodeGrant(
- identifier, secret, authorizationEndpoint, tokenEndpoint);
+ identifier, authorizationEndpoint, tokenEndpoint,
+ secret: secret);
// Redirect the resource owner to the authorization URL. This will be a URL on
// the authorization server (authorizationEndpoint with some additional query
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index e3bb645..7a73d44 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -68,6 +68,9 @@
/// documentation.
final Uri tokenEndpoint;
+ /// Whether to use HTTP Basic authentication for authorizing the client.
+ final bool _basicAuth;
+
/// The HTTP client used to make HTTP requests.
http.Client _httpClient;
@@ -87,15 +90,23 @@
/// Creates a new grant.
///
+ /// If [basicAuth] is `true` (the default), the client credentials are sent to
+ /// the server using using HTTP Basic authentication as defined in [RFC 2617].
+ /// Otherwise, they're included in the request body. Note that the latter form
+ /// is not recommended by the OAuth 2.0 spec, and should only be used if the
+ /// server doesn't support Basic authentication.
+ ///
+ /// [RFC 2617]: https://tools.ietf.org/html/rfc2617
+ ///
/// [httpClient] is used for all HTTP requests made by this grant, as well as
/// those of the [Client] is constructs.
AuthorizationCodeGrant(
this.identifier,
- this.secret,
this.authorizationEndpoint,
this.tokenEndpoint,
- {http.Client httpClient})
- : _httpClient = httpClient == null ? new http.Client() : httpClient;
+ {this.secret, bool basicAuth: true, http.Client httpClient})
+ : _basicAuth = basicAuth,
+ _httpClient = httpClient == null ? new http.Client() : httpClient;
/// Returns the URL to which the resource owner should be redirected to
/// authorize this client.
@@ -116,13 +127,19 @@
/// query parameters provided to the redirect URL.
///
/// It is a [StateError] to call this more than once.
- Uri getAuthorizationUrl(Uri redirect,
- {List<String> scopes: const <String>[], String state}) {
+ Uri getAuthorizationUrl(Uri redirect, {Iterable<String> scopes,
+ String state}) {
if (_state != _State.initial) {
throw new StateError('The authorization URL has already been generated.');
}
_state = _State.awaitingResponse;
+ if (scopes == null) {
+ scopes = [];
+ } else {
+ scopes = scopes.toList();
+ }
+
this._redirectEndpoint = redirect;
this._scopes = scopes;
this._stateString = state;
@@ -224,21 +241,35 @@
/// the state beforehand.
Future<Client> _handleAuthorizationCode(String authorizationCode) async {
var startTime = new DateTime.now();
- var response = await _httpClient.post(this.tokenEndpoint, body: {
+
+ var headers = {};
+
+ var body = {
"grant_type": "authorization_code",
"code": authorizationCode,
- "redirect_uri": this._redirectEndpoint.toString(),
- // TODO(nweiz): the spec recommends that HTTP basic auth be used in
- // preference to form parameters, but Google doesn't support that. Should
- // it be configurable?
- "client_id": this.identifier,
- "client_secret": this.secret
- });
+ "redirect_uri": this._redirectEndpoint.toString()
+ };
+
+ if (_basicAuth && secret != null) {
+ headers["Authorization"] = basicAuthHeader(identifier, secret);
+ } else {
+ // The ID is required for this request any time basic auth isn't being
+ // used, even if there's no actual client authentication to be done.
+ body["client_id"] = identifier;
+ if (secret != null) body["client_secret"] = secret;
+ }
+
+ var response = await _httpClient.post(this.tokenEndpoint,
+ headers: headers, body: body);
var credentials = handleAccessTokenResponse(
response, tokenEndpoint, startTime, _scopes);
return new Client(
- this.identifier, this.secret, credentials, httpClient: _httpClient);
+ credentials,
+ identifier: this.identifier,
+ secret: this.secret,
+ basicAuth: _basicAuth,
+ httpClient: _httpClient);
}
/// Closes the grant and frees its resources.
diff --git a/lib/src/client.dart b/lib/src/client.dart
index 371eed3..adf53d5 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -7,6 +7,7 @@
import 'dart:async';
import 'package:http/http.dart' as http;
+import 'package:http_parser/http_parser.dart';
import 'authorization_exception.dart';
import 'credentials.dart';
@@ -69,6 +70,9 @@
Credentials get credentials => _credentials;
Credentials _credentials;
+ /// Whether to use HTTP Basic authentication for authorizing the client.
+ final bool _basicAuth;
+
/// The underlying HTTP client.
http.Client _httpClient;
@@ -79,12 +83,16 @@
///
/// [httpClient] is the underlying client that this forwards requests to after
/// adding authorization credentials to them.
- Client(
- this.identifier,
- this.secret,
- this._credentials,
- {http.Client httpClient})
- : _httpClient = httpClient == null ? new http.Client() : httpClient;
+ ///
+ /// Throws an [ArgumentError] if [secret] is passed without [identifier].
+ Client(this._credentials, {this.identifier, this.secret,
+ bool basicAuth: true, http.Client httpClient})
+ : _basicAuth = basicAuth,
+ _httpClient = httpClient == null ? new http.Client() : httpClient {
+ if (identifier == null && secret != null) {
+ throw new ArgumentError("secret may not be passed without identifier.");
+ }
+ }
/// Sends an HTTP request with OAuth2 authorization credentials attached.
///
@@ -102,17 +110,19 @@
if (response.statusCode != 401) return response;
if (!response.headers.containsKey('www-authenticate')) return response;
- var authenticate;
+ var challenges;
try {
- authenticate = new AuthenticateHeader.parse(
+ challenges = AuthenticationChallenge.parseHeader(
response.headers['www-authenticate']);
} on FormatException catch (_) {
return response;
}
- if (authenticate.scheme != 'bearer') return response;
+ var challenge = challenges.firstWhere(
+ (challenge) => challenge.scheme == 'bearer', orElse: () => null);
+ if (challenge == null) return response;
- var params = authenticate.parameters;
+ var params = challenge.parameters;
if (!params.containsKey('error')) return response;
throw new AuthorizationException(
@@ -137,8 +147,11 @@
}
_credentials = await credentials.refresh(
- identifier, secret,
- newScopes: newScopes, httpClient: _httpClient);
+ identifier: identifier,
+ secret: secret,
+ newScopes: newScopes,
+ basicAuth: _basicAuth,
+ httpClient: _httpClient);
return this;
}
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index 37e7114..0a12f3f 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -5,11 +5,13 @@
library oauth2.credentials;
import 'dart:async';
+import 'dart:collection';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'handle_access_token_response.dart';
+import 'utils.dart';
/// Credentials that prove that a client is allowed to access a resource on the
/// resource owner's behalf.
@@ -72,11 +74,15 @@
/// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized
/// form via [Credentials.fromJson].
Credentials(
- this.accessToken,
- [this.refreshToken,
- this.tokenEndpoint,
- this.scopes,
- this.expiration]);
+ this.accessToken,
+ {this.refreshToken,
+ this.tokenEndpoint,
+ Iterable<String> scopes,
+ this.expiration})
+ : scopes = new UnmodifiableListView(
+ // Explicitly type-annotate the list literal to work around
+ // sdk#24202.
+ scopes == null ? <String>[] : scopes.toList());
/// Loads a set of credentials from a JSON-serialized form.
///
@@ -126,10 +132,10 @@
return new Credentials(
parsed['accessToken'],
- parsed['refreshToken'],
- tokenEndpoint,
- scopes,
- expiration);
+ refreshToken: parsed['refreshToken'],
+ tokenEndpoint: tokenEndpoint,
+ scopes: scopes,
+ expiration: expiration);
}
/// Serializes a set of credentials to JSON.
@@ -152,19 +158,25 @@
/// You may request different scopes than the default by passing in
/// [newScopes]. These must be a subset of [scopes].
///
- /// This will throw a [StateError] if these credentials can't be refreshed, an
+ /// This throws an [ArgumentError] if [secret] is passed without [identifier],
+ /// a [StateError] if these credentials can't be refreshed, an
/// [AuthorizationException] if refreshing the credentials fails, or a
/// [FormatError] if the authorization server returns invalid responses.
Future<Credentials> refresh(
- String identifier,
+ {String identifier,
String secret,
- {List<String> newScopes,
- http.Client httpClient}) async {
+ Iterable<String> newScopes,
+ bool basicAuth: true,
+ http.Client httpClient}) async {
var scopes = this.scopes;
- if (newScopes != null) scopes = newScopes;
- if (scopes == null) scopes = <String>[];
+ if (newScopes != null) scopes = newScopes.toList();
+ if (scopes == null) scopes = [];
if (httpClient == null) httpClient = new http.Client();
+ if (identifier == null && secret != null) {
+ throw new ArgumentError("secret may not be passed without identifier.");
+ }
+
var startTime = new DateTime.now();
if (refreshToken == null) {
throw new StateError("Can't refresh credentials without a refresh "
@@ -174,29 +186,34 @@
"endpoint.");
}
- var fields = {
- "grant_type": "refresh_token",
- "refresh_token": refreshToken,
- // TODO(nweiz): the spec recommends that HTTP basic auth be used in
- // preference to form parameters, but Google doesn't support that.
- // Should it be configurable?
- "client_id": identifier,
- "client_secret": secret
- };
- if (!scopes.isEmpty) fields["scope"] = scopes.join(' ');
+ var headers = {};
- var response = await httpClient.post(tokenEndpoint, body: fields);
+ var body = {
+ "grant_type": "refresh_token",
+ "refresh_token": refreshToken
+ };
+ if (!scopes.isEmpty) body["scope"] = scopes.join(' ');
+
+ if (basicAuth && secret != null) {
+ headers["Authorization"] = basicAuthHeader(identifier, secret);
+ } else {
+ if (identifier != null) body["client_id"] = identifier;
+ if (secret != null) body["client_secret"] = secret;
+ }
+
+ var response = await httpClient.post(tokenEndpoint,
+ headers: headers, body: body);
var credentials = await handleAccessTokenResponse(
- response, tokenEndpoint, startTime, scopes);
+ response, tokenEndpoint, startTime, scopes);
// The authorization server may issue a new refresh token. If it doesn't,
// we should re-use the one we already have.
if (credentials.refreshToken != null) return credentials;
return new Credentials(
credentials.accessToken,
- this.refreshToken,
- credentials.tokenEndpoint,
- credentials.scopes,
- credentials.expiration);
+ refreshToken: this.refreshToken,
+ tokenEndpoint: credentials.tokenEndpoint,
+ scopes: credentials.scopes,
+ expiration: credentials.expiration);
}
}
diff --git a/lib/src/handle_access_token_response.dart b/lib/src/handle_access_token_response.dart
index 0065f0c..ef9b198 100644
--- a/lib/src/handle_access_token_response.dart
+++ b/lib/src/handle_access_token_response.dart
@@ -81,10 +81,10 @@
return new Credentials(
parameters['access_token'],
- parameters['refresh_token'],
- tokenEndpoint,
- scopes,
- expiration);
+ refreshToken: parameters['refresh_token'],
+ tokenEndpoint: tokenEndpoint,
+ scopes: scopes,
+ expiration: expiration);
}
/// Throws the appropriate exception for an error response from the
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 734c58e..bf260eb 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -4,65 +4,16 @@
library oauth2.utils;
+import 'dart:convert';
+
+import 'package:crypto/crypto.dart';
+
/// Adds additional query parameters to [url], overwriting the original
/// parameters if a name conflict occurs.
Uri addQueryParameters(Uri url, Map<String, String> parameters) => url.replace(
queryParameters: new Map.from(url.queryParameters)..addAll(parameters));
-/// Like [String.split], but only splits on the first occurrence of the pattern.
-///
-/// This will always return a list of two elements or fewer.
-List<String> split1(String toSplit, String pattern) {
- if (toSplit.isEmpty) return [];
-
- var index = toSplit.indexOf(pattern);
- if (index == -1) return [toSplit];
- return [toSplit.substring(0, index),
- toSplit.substring(index + pattern.length)];
-}
-
-/// A WWW-Authenticate header value, parsed as per [RFC 2617][].
-///
-/// [RFC 2617]: http://tools.ietf.org/html/rfc2617
-class AuthenticateHeader {
- final String scheme;
- final Map<String, String> parameters;
-
- AuthenticateHeader(this.scheme, this.parameters);
-
- /// Parses a header string. Throws a [FormatException] if the header is
- /// invalid.
- factory AuthenticateHeader.parse(String header) {
- var split = split1(header, ' ');
- if (split.length == 0) {
- throw new FormatException('Invalid WWW-Authenticate header: "$header"');
- } else if (split.length == 1 || split[1].trim().isEmpty) {
- return new AuthenticateHeader(split[0].toLowerCase(), {});
- }
- var scheme = split[0].toLowerCase();
- var paramString = split[1];
-
- // From http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html.
- var tokenChar = r'[^\0-\x1F()<>@,;:\\"/\[\]?={} \t\x7F]';
- var quotedStringChar = r'(?:[^\0-\x1F\x7F"]|\\.)';
- var regexp = new RegExp('^ *($tokenChar+)="($quotedStringChar*)" *(, *)?');
-
- var parameters = {};
- var match;
- do {
- match = regexp.firstMatch(paramString);
- if (match == null) {
- throw new FormatException('Invalid WWW-Authenticate header: "$header"');
- }
-
- paramString = paramString.substring(match.end);
- parameters[match.group(1).toLowerCase()] = match.group(2);
- } while (match.group(3) != null);
-
- if (!paramString.trim().isEmpty) {
- throw new FormatException('Invalid WWW-Authenticate header: "$header"');
- }
-
- return new AuthenticateHeader(scheme, parameters);
- }
+String basicAuthHeader(String identifier, String secret) {
+ var userPass = Uri.encodeFull(identifier) + ":" + Uri.encodeFull(secret);
+ return "Basic " + CryptoUtils.bytesToBase64(ASCII.encode(userPass));
}
diff --git a/pubspec.yaml b/pubspec.yaml
index e40265a..f52b8be 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: oauth2
-version: 0.9.4-dev
+version: 1.0.0-dev
author: Dart Team <misc@dartlang.org>
homepage: http://github.com/dart-lang/oauth2
description: >
@@ -10,6 +10,6 @@
sdk: '>=1.9.0 <2.0.0'
dependencies:
http: '>=0.11.0 <0.12.0'
- http_parser: '>=0.0.0 <0.1.0'
+ http_parser: '^1.0.0'
dev_dependencies:
test: '>=0.12.0 <0.13.0'
diff --git a/test/authorization_code_grant_test.dart b/test/authorization_code_grant_test.dart
index deaad5c..e03a607 100644
--- a/test/authorization_code_grant_test.dart
+++ b/test/authorization_code_grant_test.dart
@@ -20,9 +20,9 @@
client = new ExpectClient();
grant = new oauth2.AuthorizationCodeGrant(
'identifier',
- 'secret',
Uri.parse('https://example.com/authorization'),
Uri.parse('https://example.com/token'),
+ secret: 'secret',
httpClient: client);
});
@@ -60,9 +60,9 @@
test('merges with existing query parameters', () {
grant = new oauth2.AuthorizationCodeGrant(
'identifier',
- 'secret',
Uri.parse('https://example.com/authorization?query=value'),
Uri.parse('https://example.com/token'),
+ secret: 'secret',
httpClient: client);
var authorizationUrl = grant.getAuthorizationUrl(redirectUrl);
@@ -127,10 +127,11 @@
expect(request.bodyFields, equals({
'grant_type': 'authorization_code',
'code': 'auth code',
- 'redirect_uri': redirectUrl.toString(),
- 'client_id': 'identifier',
- 'client_secret': 'secret'
+ 'redirect_uri': redirectUrl.toString()
}));
+ expect(request.headers, containsPair(
+ "Authorization",
+ "Basic aWRlbnRpZmllcjpzZWNyZXQ="));
return new Future.value(new http.Response(JSON.encode({
'access_token': 'access token',
@@ -163,6 +164,71 @@
expect(request.bodyFields, equals({
'grant_type': 'authorization_code',
'code': 'auth code',
+ 'redirect_uri': redirectUrl.toString()
+ }));
+ expect(request.headers, containsPair(
+ "Authorization",
+ "Basic aWRlbnRpZmllcjpzZWNyZXQ="));
+
+ return new Future.value(new http.Response(JSON.encode({
+ 'access_token': 'access token',
+ 'token_type': 'bearer',
+ }), 200, headers: {'content-type': 'application/json'}));
+ });
+
+ expect(grant.handleAuthorizationCode('auth code'),
+ completion(predicate((client) {
+ expect(client.credentials.accessToken, equals('access token'));
+ return true;
+ })));
+ });
+ });
+
+ group("with basicAuth: false", () {
+ setUp(() {
+ client = new ExpectClient();
+ grant = new oauth2.AuthorizationCodeGrant(
+ 'identifier',
+ Uri.parse('https://example.com/authorization'),
+ Uri.parse('https://example.com/token'),
+ secret: 'secret',
+ basicAuth: false,
+ httpClient: client);
+ });
+
+ test('.handleAuthorizationResponse sends an authorization code request',
+ () {
+ grant.getAuthorizationUrl(redirectUrl);
+ client.expectRequest((request) {
+ expect(request.method, equals('POST'));
+ expect(request.url.toString(), equals(grant.tokenEndpoint.toString()));
+ expect(request.bodyFields, equals({
+ 'grant_type': 'authorization_code',
+ 'code': 'auth code',
+ 'redirect_uri': redirectUrl.toString(),
+ 'client_id': 'identifier',
+ 'client_secret': 'secret'
+ }));
+
+ return new Future.value(new http.Response(JSON.encode({
+ 'access_token': 'access token',
+ 'token_type': 'bearer',
+ }), 200, headers: {'content-type': 'application/json'}));
+ });
+
+ expect(grant.handleAuthorizationResponse({'code': 'auth code'})
+ .then((client) => client.credentials.accessToken),
+ completion(equals('access token')));
+ });
+
+ test('.handleAuthorizationCode sends an authorization code request', () {
+ grant.getAuthorizationUrl(redirectUrl);
+ client.expectRequest((request) {
+ expect(request.method, equals('POST'));
+ expect(request.url.toString(), equals(grant.tokenEndpoint.toString()));
+ expect(request.bodyFields, equals({
+ 'grant_type': 'authorization_code',
+ 'code': 'auth code',
'redirect_uri': redirectUrl.toString(),
'client_id': 'identifier',
'client_secret': 'secret'
diff --git a/test/client_test.dart b/test/client_test.dart
index 969787a..7dc5afd 100644
--- a/test/client_test.dart
+++ b/test/client_test.dart
@@ -23,8 +23,10 @@
test("that can't be refreshed throws an ExpirationException on send", () {
var expiration = new DateTime.now().subtract(new Duration(hours: 1));
var credentials = new oauth2.Credentials(
- 'access token', null, null, [], expiration);
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ 'access token', expiration: expiration);
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
expect(client.get(requestUri),
@@ -35,8 +37,13 @@
"request", () async {
var expiration = new DateTime.now().subtract(new Duration(hours: 1));
var credentials = new oauth2.Credentials(
- 'access token', 'refresh token', tokenEndpoint, [], expiration);
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint,
+ expiration: expiration);
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
@@ -65,7 +72,9 @@
group('with valid credentials', () {
test("sends a request with bearer authorization", () {
var credentials = new oauth2.Credentials('access token');
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
@@ -81,8 +90,12 @@
test("can manually refresh the credentials", () async {
var credentials = new oauth2.Credentials(
- 'access token', 'refresh token', tokenEndpoint);
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint);
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
@@ -100,7 +113,9 @@
test("without a refresh token can't manually refresh the credentials", () {
var credentials = new oauth2.Credentials('access token');
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
expect(client.refreshCredentials(), throwsA(isStateError));
@@ -110,7 +125,9 @@
group('with invalid credentials', () {
test('throws an AuthorizationException for a 401 response', () {
var credentials = new oauth2.Credentials('access token');
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
@@ -130,7 +147,9 @@
test('passes through a 401 response without www-authenticate', () async {
var credentials = new oauth2.Credentials('access token');
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
@@ -148,7 +167,9 @@
test('passes through a 401 response with invalid www-authenticate',
() async {
var credentials = new oauth2.Credentials('access token');
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
@@ -157,8 +178,8 @@
expect(request.headers['authorization'],
equals('Bearer access token'));
- var authenticate = 'Bearer error="invalid_token", error_description='
- '"Something is terribly wrong.", ';
+ var authenticate = 'Bearer error="invalid_token" error_description='
+ '"Something is terribly wrong."';
return new Future.value(new http.Response('bad job', 401,
headers: {'www-authenticate': authenticate}));
});
@@ -169,7 +190,9 @@
test('passes through a 401 response with non-bearer www-authenticate',
() async {
var credentials = new oauth2.Credentials('access token');
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
@@ -188,7 +211,9 @@
test('passes through a 401 response with non-OAuth2 www-authenticate',
() async {
var credentials = new oauth2.Credentials('access token');
- var client = new oauth2.Client('identifier', 'secret', credentials,
+ var client = new oauth2.Client(credentials,
+ identifier: 'identifier',
+ secret: 'secret',
httpClient: httpClient);
httpClient.expectRequest((request) {
diff --git a/test/credentials_test.dart b/test/credentials_test.dart
index d72614f..f4d8886 100644
--- a/test/credentials_test.dart
+++ b/test/credentials_test.dart
@@ -25,37 +25,82 @@
test('is not expired if the expiration is in the future', () {
var expiration = new DateTime.now().add(new Duration(hours: 1));
var credentials = new oauth2.Credentials(
- 'access token', null, null, null, expiration);
+ 'access token', expiration: expiration);
expect(credentials.isExpired, isFalse);
});
test('is expired if the expiration is in the past', () {
var expiration = new DateTime.now().subtract(new Duration(hours: 1));
var credentials = new oauth2.Credentials(
- 'access token', null, null, null, expiration);
+ 'access token', expiration: expiration);
expect(credentials.isExpired, isTrue);
});
test("can't refresh without a refresh token", () {
var credentials = new oauth2.Credentials(
- 'access token', null, tokenEndpoint);
+ 'access token', tokenEndpoint: tokenEndpoint);
expect(credentials.canRefresh, false);
- expect(credentials.refresh('identifier', 'secret', httpClient: httpClient),
+ expect(credentials.refresh(
+ identifier: 'identifier',
+ secret: 'secret',
+ httpClient: httpClient),
throwsStateError);
});
test("can't refresh without a token endpoint", () {
- var credentials = new oauth2.Credentials('access token', 'refresh token');
+ var credentials = new oauth2.Credentials(
+ 'access token', refreshToken: 'refresh token');
expect(credentials.canRefresh, false);
- expect(credentials.refresh('identifier', 'secret', httpClient: httpClient),
+ expect(credentials.refresh(
+ identifier: 'identifier',
+ secret: 'secret',
+ httpClient: httpClient),
throwsStateError);
});
test("can refresh with a refresh token and a token endpoint", () async {
var credentials = new oauth2.Credentials(
- 'access token', 'refresh token', tokenEndpoint, ['scope1', 'scope2']);
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint,
+ scopes: ['scope1', 'scope2']);
+ expect(credentials.canRefresh, true);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('POST'));
+ expect(request.url.toString(), equals(tokenEndpoint.toString()));
+ expect(request.bodyFields, equals({
+ "grant_type": "refresh_token",
+ "refresh_token": "refresh token",
+ "scope": "scope1 scope2"
+ }));
+ expect(request.headers, containsPair(
+ "Authorization",
+ "Basic aWQlQzMlQUJudCVDNCVBQmZpZXI6cyVDMyVBQmNyZXQ="));
+
+ 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'}));
+ });
+
+ credentials = await credentials.refresh(
+ identifier: 'idëntīfier',
+ secret: 'sëcret',
+ httpClient: httpClient);
+ expect(credentials.accessToken, equals('new access token'));
+ expect(credentials.refreshToken, equals('new refresh token'));
+ });
+
+ test("can refresh without a client secret", () async {
+ var credentials = new oauth2.Credentials(
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint,
+ scopes: ['scope1', 'scope2']);
expect(credentials.canRefresh, true);
httpClient.expectRequest((request) {
@@ -65,8 +110,7 @@
"grant_type": "refresh_token",
"refresh_token": "refresh token",
"scope": "scope1 scope2",
- "client_id": "identifier",
- "client_secret": "secret"
+ "client_id": "identifier"
}));
return new Future.value(new http.Response(JSON.encode({
@@ -77,15 +121,19 @@
});
- credentials = await credentials.refresh('identifier', 'secret',
+ credentials = await credentials.refresh(
+ identifier: 'identifier',
httpClient: httpClient);
expect(credentials.accessToken, equals('new access token'));
expect(credentials.refreshToken, equals('new refresh token'));
});
- test("uses the old refresh token if a new one isn't provided", () async {
+ test("can refresh without client authentication", () async {
var credentials = new oauth2.Credentials(
- 'access token', 'refresh token', tokenEndpoint);
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint,
+ scopes: ['scope1', 'scope2']);
expect(credentials.canRefresh, true);
httpClient.expectRequest((request) {
@@ -94,23 +142,90 @@
expect(request.bodyFields, equals({
"grant_type": "refresh_token",
"refresh_token": "refresh token",
- "client_id": "identifier",
- "client_secret": "secret"
+ "scope": "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'}));
+ });
+
+
+ credentials = await credentials.refresh(httpClient: httpClient);
+ expect(credentials.accessToken, equals('new access token'));
+ expect(credentials.refreshToken, equals('new refresh token'));
+ });
+
+ test("uses the old refresh token if a new one isn't provided", () async {
+ var credentials = new oauth2.Credentials(
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint);
+ expect(credentials.canRefresh, true);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('POST'));
+ expect(request.url.toString(), equals(tokenEndpoint.toString()));
+ expect(request.bodyFields, equals({
+ "grant_type": "refresh_token",
+ "refresh_token": "refresh token"
+ }));
+ expect(request.headers, containsPair(
+ "Authorization",
+ "Basic aWQlQzMlQUJudCVDNCVBQmZpZXI6cyVDMyVBQmNyZXQ="));
+
+ return new Future.value(new http.Response(JSON.encode({
+ 'access_token': 'new access token',
'token_type': 'bearer'
}), 200, headers: {'content-type': 'application/json'}));
});
- credentials = await credentials.refresh('identifier', 'secret',
+ credentials = await credentials.refresh(
+ identifier: 'idëntīfier',
+ secret: 'sëcret',
httpClient: httpClient);
expect(credentials.accessToken, equals('new access token'));
expect(credentials.refreshToken, equals('refresh token'));
});
+ test("uses form-field authentication if basicAuth is false", () async {
+ var credentials = new oauth2.Credentials(
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint,
+ scopes: ['scope1', 'scope2']);
+ expect(credentials.canRefresh, true);
+
+ httpClient.expectRequest((request) {
+ expect(request.method, equals('POST'));
+ expect(request.url.toString(), equals(tokenEndpoint.toString()));
+ expect(request.bodyFields, equals({
+ "grant_type": "refresh_token",
+ "refresh_token": "refresh token",
+ "scope": "scope1 scope2",
+ "client_id": "idëntīfier",
+ "client_secret": "sëcret"
+ }));
+
+ 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'}));
+ });
+
+ credentials = await credentials.refresh(
+ identifier: 'idëntīfier',
+ secret: 'sëcret',
+ basicAuth: false,
+ httpClient: httpClient);
+ expect(credentials.accessToken, equals('new access token'));
+ expect(credentials.refreshToken, equals('new refresh token'));
+ });
+
group("fromJson", () {
oauth2.Credentials fromMap(Map map) =>
new oauth2.Credentials.fromJson(JSON.encode(map));
@@ -118,8 +233,11 @@
test("should load the same credentials from toJson", () {
var expiration = new DateTime.now().subtract(new Duration(hours: 1));
var credentials = new oauth2.Credentials(
- 'access token', 'refresh token', tokenEndpoint, ['scope1', 'scope2'],
- expiration);
+ 'access token',
+ refreshToken: 'refresh token',
+ tokenEndpoint: tokenEndpoint,
+ scopes: ['scope1', 'scope2'],
+ expiration: expiration);
var reloaded = new oauth2.Credentials.fromJson(credentials.toJson());
expect(reloaded.accessToken, equals(credentials.accessToken));
diff --git a/test/utils_test.dart b/test/utils_test.dart
deleted file mode 100644
index 54c2da5..0000000
--- a/test/utils_test.dart
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) 2012, 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 'package:oauth2/src/utils.dart';
-import 'package:test/test.dart';
-
-void main() {
- group('AuthenticateHeader', () {
- test("parses a scheme", () {
- var header = new AuthenticateHeader.parse('bearer');
- expect(header.scheme, equals('bearer'));
- expect(header.parameters, equals({}));
- });
-
- test("lower-cases the scheme", () {
- var header = new AuthenticateHeader.parse('BeaRer');
- expect(header.scheme, equals('bearer'));
- expect(header.parameters, equals({}));
- });
-
- test("parses a scheme with trailing whitespace", () {
- var header = new AuthenticateHeader.parse('bearer ');
- expect(header.scheme, equals('bearer'));
- expect(header.parameters, equals({}));
- });
-
- test("parses a scheme with one param", () {
- var header = new AuthenticateHeader.parse('bearer foo="bar"');
- expect(header.scheme, equals('bearer'));
- expect(header.parameters, equals({'foo': 'bar'}));
- });
-
- test("parses a scheme with several params", () {
- var header = new AuthenticateHeader.parse(
- 'bearer foo="bar", bar="baz" ,baz="qux"');
- expect(header.scheme, equals('bearer'));
- expect(header.parameters, equals({
- 'foo': 'bar',
- 'bar': 'baz',
- 'baz': 'qux'
- }));
- });
-
- test("lower-cases parameter names but not values", () {
- var header = new AuthenticateHeader.parse('bearer FoO="bAr"');
- expect(header.scheme, equals('bearer'));
- expect(header.parameters, equals({'foo': 'bAr'}));
- });
-
- test("allows empty values", () {
- var header = new AuthenticateHeader.parse('bearer foo=""');
- expect(header.scheme, equals('bearer'));
- expect(header.parameters, equals({'foo': ''}));
- });
-
- test("won't parse an empty string", () {
- expect(() => new AuthenticateHeader.parse(''),
- throwsFormatException);
- });
-
- test("won't parse a token without a value", () {
- expect(() => new AuthenticateHeader.parse('bearer foo'),
- throwsFormatException);
-
- expect(() => new AuthenticateHeader.parse('bearer foo='),
- throwsFormatException);
- });
-
- test("won't parse a token without a value", () {
- expect(() => new AuthenticateHeader.parse('bearer foo'),
- throwsFormatException);
-
- expect(() => new AuthenticateHeader.parse('bearer foo='),
- throwsFormatException);
- });
-
- test("won't parse a trailing comma", () {
- expect(() => new AuthenticateHeader.parse('bearer foo="bar",'),
- throwsFormatException);
- });
-
- test("won't parse a multiple params without a comma", () {
- expect(() => new AuthenticateHeader.parse('bearer foo="bar" bar="baz"'),
- throwsFormatException);
- });
- });
-}