Async-ify.
R=rnystrom@google.com
Review URL: https://codereview.chromium.org//1308163004 .
diff --git a/README.md b/README.md
index 52b724c..23be5ff 100644
--- a/README.md
+++ b/README.md
@@ -48,53 +48,60 @@
// query parameters.
final redirectUrl = Uri.parse("http://my-site.com/oauth2-redirect");
-var credentialsFile = new File("~/.myapp/credentials.json");
-return credentialsFile.exists().then((exists) {
- // If the OAuth2 credentials have already been saved from a previous
- // run, we just want to reload them.
+/// A file in which the users credentials are stored persistently. If the server
+/// issues a refresh token allowing the client to refresh outdated credentials,
+/// these may be valid indefinitely, meaning the user never has to
+/// re-authenticate.
+final credentialsFile = new File("~/.myapp/credentials.json");
+
+/// Either load an OAuth2 client from saved credentials or authenticate a new
+/// one.
+Future<oauth2.Client> getClient() async {
+ var exists = await credentialsFile.exists();
+
+ // If the OAuth2 credentials have already been saved from a previous run, we
+ // just want to reload them.
if (exists) {
- return credentialsFile.readAsString().then((json) {
- var credentials = new oauth2.Credentials.fromJson(json);
- return new oauth2.Client(identifier, secret, credentials);
- });
+ var credentials = new oauth2.Credentials.fromJson(
+ await credentialsFile.readAsString());
+ return new oauth2.Client(identifier, secret, credentials);
}
- // 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.
+ // 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);
- // Redirect the resource owner to the authorization URL. This will be
- // a URL on the authorization server (authorizationEndpoint with some
- // additional query parameters). Once the resource owner has
- // authorized, they'll be redirected to `redirectUrl` with an
- // authorization code.
+ // Redirect the resource owner to the authorization URL. This will be a URL on
+ // the authorization server (authorizationEndpoint with some additional query
+ // parameters). Once the resource owner has authorized, they'll be redirected
+ // to `redirectUrl` with an authorization code.
//
// `redirect` is an imaginary function that redirects the resource
// owner's browser.
- return redirect(grant.getAuthorizationUrl(redirectUrl)).then((_) {
- // Another imaginary function that listens for a request to
- // `redirectUrl`.
- return listen(redirectUrl);
- }).then((request) {
- // Once the user is redirected to `redirectUrl`, pass the query
- // parameters to the AuthorizationCodeGrant. It will validate them
- // and extract the authorization code to create a new Client.
- return grant.handleAuthorizationResponse(request.uri.queryParameters);
- })
-}).then((client) {
- // Once you have a Client, you can use it just like any other HTTP
- // client.
- return client.read("http://example.com/protected-resources.txt")
- .then((result) {
- // Once we're done with the client, save the credentials file. This
- // ensures that if the credentials were automatically refreshed
- // while using the client, the new credentials are available for the
- // next run of the program.
- return credentialsFile.open(FileMode.WRITE).then((file) {
- return file.writeString(client.credentials.toJson());
- }).then((file) => file.close()).then((_) => result);
- });
-}).then(print);
+ await redirect(grant.getAuthorizationUrl(redirectUrl));
+
+ // Another imaginary function that listens for a request to `redirectUrl`.
+ var request = await listen(redirectUrl);
+
+ // Once the user is redirected to `redirectUrl`, pass the query parameters to
+ // the AuthorizationCodeGrant. It will validate them and extract the
+ // authorization code to create a new Client.
+ return await grant.handleAuthorizationResponse(request.uri.queryParameters);
+}
+
+main() async {
+ var client = await loadClient();
+
+ // Once you have a Client, you can use it just like any other HTTP client.
+ var result = client.read("http://example.com/protected-resources.txt");
+
+ // Once we're done with the client, save the credentials file. This ensures
+ // that if the credentials were automatically refreshed while using the
+ // client, the new credentials are available for the next run of the
+ // program.
+ await credentialsFile.writeAsString(client.credentials.toJson());
+
+ print(result);
+}
```
diff --git a/lib/src/authorization_code_grant.dart b/lib/src/authorization_code_grant.dart
index c879e8c..9ec4a27 100644
--- a/lib/src/authorization_code_grant.dart
+++ b/lib/src/authorization_code_grant.dart
@@ -96,12 +96,12 @@
/// [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.identifier,
+ this.secret,
+ this.authorizationEndpoint,
+ this.tokenEndpoint,
+ {http.Client httpClient})
+ : _httpClient = httpClient == null ? new http.Client() : httpClient;
/// Returns the URL to which the resource owner should be redirected to
/// authorize this client. The resource owner will then be redirected to
@@ -157,42 +157,41 @@
/// [FormatError] if the `state` parameter doesn't match the original value.
///
/// Throws [AuthorizationException] if the authorization fails.
- Future<Client> handleAuthorizationResponse(Map<String, String> parameters) {
- return async.then((_) {
- if (_state == _INITIAL_STATE) {
- throw new StateError(
- 'The authorization URL has not yet been generated.');
- } else if (_state == _FINISHED_STATE) {
- throw new StateError(
- 'The authorization code has already been received.');
- }
- _state = _FINISHED_STATE;
+ Future<Client> handleAuthorizationResponse(Map<String, String> parameters)
+ async {
+ if (_state == _INITIAL_STATE) {
+ throw new StateError(
+ 'The authorization URL has not yet been generated.');
+ } else if (_state == _FINISHED_STATE) {
+ throw new StateError(
+ 'The authorization code has already been received.');
+ }
+ _state = _FINISHED_STATE;
- if (_stateString != null) {
- if (!parameters.containsKey('state')) {
- throw new FormatException('Invalid OAuth response for '
- '"$authorizationEndpoint": parameter "state" expected to be '
- '"$_stateString", was missing.');
- } else if (parameters['state'] != _stateString) {
- throw new FormatException('Invalid OAuth response for '
- '"$authorizationEndpoint": parameter "state" expected to be '
- '"$_stateString", was "${parameters['state']}".');
- }
- }
-
- if (parameters.containsKey('error')) {
- var description = parameters['error_description'];
- var uriString = parameters['error_uri'];
- var uri = uriString == null ? null : Uri.parse(uriString);
- throw new AuthorizationException(parameters['error'], description, uri);
- } else if (!parameters.containsKey('code')) {
+ if (_stateString != null) {
+ if (!parameters.containsKey('state')) {
throw new FormatException('Invalid OAuth response for '
- '"$authorizationEndpoint": did not contain required parameter '
- '"code".');
+ '"$authorizationEndpoint": parameter "state" expected to be '
+ '"$_stateString", was missing.');
+ } else if (parameters['state'] != _stateString) {
+ throw new FormatException('Invalid OAuth response for '
+ '"$authorizationEndpoint": parameter "state" expected to be '
+ '"$_stateString", was "${parameters['state']}".');
}
+ }
- return _handleAuthorizationCode(parameters['code']);
- });
+ if (parameters.containsKey('error')) {
+ var description = parameters['error_description'];
+ var uriString = parameters['error_uri'];
+ var uri = uriString == null ? null : Uri.parse(uriString);
+ throw new AuthorizationException(parameters['error'], description, uri);
+ } else if (!parameters.containsKey('code')) {
+ throw new FormatException('Invalid OAuth response for '
+ '"$authorizationEndpoint": did not contain required parameter '
+ '"code".');
+ }
+
+ return await _handleAuthorizationCode(parameters['code']);
}
/// Processes an authorization code directly. Usually
@@ -209,26 +208,24 @@
/// responses while retrieving credentials.
///
/// Throws [AuthorizationException] if the authorization fails.
- Future<Client> handleAuthorizationCode(String authorizationCode) {
- return async.then((_) {
- if (_state == _INITIAL_STATE) {
- throw new StateError(
- 'The authorization URL has not yet been generated.');
- } else if (_state == _FINISHED_STATE) {
- throw new StateError(
- 'The authorization code has already been received.');
- }
- _state = _FINISHED_STATE;
+ Future<Client> handleAuthorizationCode(String authorizationCode) async {
+ if (_state == _INITIAL_STATE) {
+ throw new StateError(
+ 'The authorization URL has not yet been generated.');
+ } else if (_state == _FINISHED_STATE) {
+ throw new StateError(
+ 'The authorization code has already been received.');
+ }
+ _state = _FINISHED_STATE;
- return _handleAuthorizationCode(authorizationCode);
- });
+ return await _handleAuthorizationCode(authorizationCode);
}
/// This works just like [handleAuthorizationCode], except it doesn't validate
/// the state beforehand.
- Future<Client> _handleAuthorizationCode(String authorizationCode) {
+ Future<Client> _handleAuthorizationCode(String authorizationCode) async {
var startTime = new DateTime.now();
- return _httpClient.post(this.tokenEndpoint, body: {
+ var response = await _httpClient.post(this.tokenEndpoint, body: {
"grant_type": "authorization_code",
"code": authorizationCode,
"redirect_uri": this._redirectEndpoint.toString(),
@@ -237,12 +234,12 @@
// it be configurable?
"client_id": this.identifier,
"client_secret": this.secret
- }).then((response) {
- var credentials = handleAccessTokenResponse(
- response, tokenEndpoint, startTime, _scopes);
- return new Client(
- this.identifier, this.secret, credentials, httpClient: _httpClient);
});
+
+ var credentials = handleAccessTokenResponse(
+ response, tokenEndpoint, startTime, _scopes);
+ return new Client(
+ this.identifier, this.secret, credentials, httpClient: _httpClient);
}
/// Closes the grant and frees its resources.
diff --git a/lib/src/client.dart b/lib/src/client.dart
index d473be1..60495a0 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -80,37 +80,34 @@
/// Sends an HTTP request with OAuth2 authorization credentials attached. This
/// will also automatically refresh this client's [Credentials] before sending
/// the request if necessary.
- Future<http.StreamedResponse> send(http.BaseRequest request) {
- return async.then((_) {
- if (!credentials.isExpired) return new Future.value();
+ Future<http.StreamedResponse> send(http.BaseRequest request) async {
+ if (credentials.isExpired) {
if (!credentials.canRefresh) throw new ExpirationException(credentials);
- return refreshCredentials();
- }).then((_) {
- request.headers['authorization'] = "Bearer ${credentials.accessToken}";
- return _httpClient.send(request);
- }).then((response) {
- if (response.statusCode != 401 ||
- !response.headers.containsKey('www-authenticate')) {
- return response;
- }
+ await refreshCredentials();
+ }
- var authenticate;
- try {
- authenticate = new AuthenticateHeader.parse(
- response.headers['www-authenticate']);
- } on FormatException catch (e) {
- return response;
- }
+ request.headers['authorization'] = "Bearer ${credentials.accessToken}";
+ var response = await _httpClient.send(request);
- if (authenticate.scheme != 'bearer') return response;
+ if (response.statusCode != 401) return response;
+ if (!response.headers.containsKey('www-authenticate')) return response;
- var params = authenticate.parameters;
- if (!params.containsKey('error')) return response;
+ var authenticate;
+ try {
+ authenticate = new AuthenticateHeader.parse(
+ response.headers['www-authenticate']);
+ } on FormatException catch (e) {
+ return response;
+ }
- throw new AuthorizationException(
- params['error'], params['error_description'],
- params['error_uri'] == null ? null : Uri.parse(params['error_uri']));
- });
+ if (authenticate.scheme != 'bearer') return response;
+
+ var params = authenticate.parameters;
+ if (!params.containsKey('error')) return response;
+
+ throw new AuthorizationException(
+ params['error'], params['error_description'],
+ params['error_uri'] == null ? null : Uri.parse(params['error_uri']));
}
/// Explicitly refreshes this client's credentials. Returns this client.
@@ -122,20 +119,18 @@
/// You may request different scopes than the default by passing in
/// [newScopes]. These must be a subset of the scopes in the
/// [Credentials.scopes] field of [Client.credentials].
- Future<Client> refreshCredentials([List<String> newScopes]) {
- return async.then((_) {
- if (!credentials.canRefresh) {
- var prefix = "OAuth credentials";
- if (credentials.isExpired) prefix = "$prefix have expired and";
- throw new StateError("$prefix can't be refreshed.");
- }
+ Future<Client> refreshCredentials([List<String> newScopes]) async {
+ if (!credentials.canRefresh) {
+ var prefix = "OAuth credentials";
+ if (credentials.isExpired) prefix = "$prefix have expired and";
+ throw new StateError("$prefix can't be refreshed.");
+ }
- return credentials.refresh(identifier, secret,
- newScopes: newScopes, httpClient: _httpClient);
- }).then((credentials) {
- _credentials = credentials;
- return this;
- });
+ _credentials = await credentials.refresh(
+ identifier, secret,
+ newScopes: newScopes, httpClient: _httpClient);
+
+ return this;
}
/// Closes this client and its underlying HTTP client.
diff --git a/lib/src/credentials.dart b/lib/src/credentials.dart
index ce4e064..88b1f5d 100644
--- a/lib/src/credentials.dart
+++ b/lib/src/credentials.dart
@@ -146,47 +146,44 @@
String identifier,
String secret,
{List<String> newScopes,
- http.Client httpClient}) {
+ http.Client httpClient}) async {
var scopes = this.scopes;
if (newScopes != null) scopes = newScopes;
if (scopes == null) scopes = <String>[];
if (httpClient == null) httpClient = new http.Client();
var startTime = new DateTime.now();
- return async.then((_) {
- if (refreshToken == null) {
- throw new StateError("Can't refresh credentials without a refresh "
- "token.");
- } else if (tokenEndpoint == null) {
- throw new StateError("Can't refresh credentials without a token "
- "endpoint.");
- }
+ if (refreshToken == null) {
+ throw new StateError("Can't refresh credentials without a refresh "
+ "token.");
+ } else if (tokenEndpoint == null) {
+ throw new StateError("Can't refresh credentials without a token "
+ "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 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(' ');
- return httpClient.post(tokenEndpoint, body: fields);
- }).then((response) {
- return handleAccessTokenResponse(
+ var response = await httpClient.post(tokenEndpoint, body: fields);
+ var credentials = await handleAccessTokenResponse(
response, tokenEndpoint, startTime, scopes);
- }).then((credentials) {
- // 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);
- });
+
+ // 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);
}
}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 12a429e..b292625 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -105,7 +105,3 @@
return new AuthenticateHeader(scheme, parameters);
}
}
-
-/// Returns a [Future] that asynchronously completes to `null`.
-Future get async => new Future.delayed(const Duration(milliseconds: 0),
- () => null);
diff --git a/pubspec.yaml b/pubspec.yaml
index 31c0b50..e40265a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -7,7 +7,7 @@
behalf of a user, and making authorized HTTP requests with the user's
OAuth2 credentials.
environment:
- sdk: '>=1.0.0 <2.0.0'
+ sdk: '>=1.9.0 <2.0.0'
dependencies:
http: '>=0.11.0 <0.12.0'
http_parser: '>=0.0.0 <0.1.0'
diff --git a/test/client_test.dart b/test/client_test.dart
index 11a3bf8..7115095 100644
--- a/test/client_test.dart
+++ b/test/client_test.dart
@@ -39,7 +39,7 @@
});
test("that can be refreshed refreshes the credentials and sends the "
- "request", () {
+ "request", () async {
var expiration = new DateTime.now().subtract(new Duration(hours: 1));
var credentials = new oauth2.Credentials(
'access token', 'refresh token', tokenEndpoint, [], expiration);
@@ -64,9 +64,8 @@
return new Future.value(new http.Response('good job', 200));
});
- expect(client.read(requestUri).then((_) {
- expect(client.credentials.accessToken, equals('new access token'));
- }), completes);
+ await client.read(requestUri);
+ expect(client.credentials.accessToken, equals('new access token'));
});
});
@@ -89,7 +88,7 @@
expect(client.read(requestUri), completion(equals('good job')));
});
- test("can manually refresh the credentials", () {
+ 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,
@@ -104,9 +103,8 @@
}), 200, headers: {'content-type': 'application/json'}));
});
- expect(client.refreshCredentials().then((_) {
- expect(client.credentials.accessToken, equals('new access token'));
- }), completes);
+ await client.refreshCredentials();
+ expect(client.credentials.accessToken, equals('new access token'));
});
test("without a refresh token can't manually refresh the credentials", () {
@@ -141,7 +139,7 @@
throwsA(new isInstanceOf<oauth2.AuthorizationException>()));
});
- test('passes through a 401 response without www-authenticate', () {
+ 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,
httpClient: httpClient);
@@ -155,12 +153,11 @@
return new Future.value(new http.Response('bad job', 401));
});
- expect(
- client.get(requestUri).then((response) => response.statusCode),
- completion(equals(401)));
+ expect((await client.get(requestUri)).statusCode, equals(401));
});
- test('passes through a 401 response with invalid www-authenticate', () {
+ 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,
httpClient: httpClient);
@@ -177,12 +174,11 @@
headers: {'www-authenticate': authenticate}));
});
- expect(
- client.get(requestUri).then((response) => response.statusCode),
- completion(equals(401)));
+ expect((await client.get(requestUri)).statusCode, equals(401));
});
- test('passes through a 401 response with non-bearer www-authenticate', () {
+ 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,
httpClient: httpClient);
@@ -197,12 +193,11 @@
headers: {'www-authenticate': 'Digest'}));
});
- expect(
- client.get(requestUri).then((response) => response.statusCode),
- completion(equals(401)));
+ expect((await client.get(requestUri)).statusCode, equals(401));
});
- test('passes through a 401 response with non-OAuth2 www-authenticate', () {
+ 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,
httpClient: httpClient);
@@ -217,9 +212,7 @@
headers: {'www-authenticate': 'Bearer'}));
});
- expect(
- client.get(requestUri).then((response) => response.statusCode),
- completion(equals(401)));
+ expect((await client.get(requestUri)).statusCode, equals(401));
});
});
}
diff --git a/test/credentials_test.dart b/test/credentials_test.dart
index 94b5445..3bd44d3 100644
--- a/test/credentials_test.dart
+++ b/test/credentials_test.dart
@@ -56,7 +56,7 @@
throwsStateError);
});
- test("can refresh with a refresh token and a token endpoint", () {
+ test("can refresh with a refresh token and a token endpoint", () async {
var credentials = new oauth2.Credentials(
'access token', 'refresh token', tokenEndpoint, ['scope1', 'scope2']);
expect(credentials.canRefresh, true);
@@ -80,14 +80,13 @@
});
- expect(credentials.refresh('identifier', 'secret', httpClient: httpClient)
- .then((credentials) {
- expect(credentials.accessToken, equals('new access token'));
- expect(credentials.refreshToken, equals('new refresh token'));
- }), completes);
+ credentials = await credentials.refresh('identifier', 'secret',
+ 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", () {
+ test("uses the old refresh token if a new one isn't provided", () async {
var credentials = new oauth2.Credentials(
'access token', 'refresh token', tokenEndpoint);
expect(credentials.canRefresh, true);
@@ -109,11 +108,10 @@
});
- expect(credentials.refresh('identifier', 'secret', httpClient: httpClient)
- .then((credentials) {
- expect(credentials.accessToken, equals('new access token'));
- expect(credentials.refreshToken, equals('refresh token'));
- }), completes);
+ credentials = await credentials.refresh('identifier', 'secret',
+ httpClient: httpClient);
+ expect(credentials.accessToken, equals('new access token'));
+ expect(credentials.refreshToken, equals('refresh token'));
});
group("fromJson", () {