Index: pkg/oauth2/lib/src/handle_access_token_response.dart |
diff --git a/pkg/oauth2/lib/src/handle_access_token_response.dart b/pkg/oauth2/lib/src/handle_access_token_response.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..af94f911f5d34027d8068aaba14f13204484c9b4 |
--- /dev/null |
+++ b/pkg/oauth2/lib/src/handle_access_token_response.dart |
@@ -0,0 +1,143 @@ |
+// 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. |
+ |
+library handle_access_token_response; |
+ |
+import 'dart:io'; |
+import 'dart:json'; |
+import 'dart:uri'; |
+ |
+import '../../../http/lib/http.dart' as http; |
+ |
+import 'credentials.dart'; |
+import 'authorization_exception.dart'; |
+ |
+/// The amount of time, in seconds, to add as a "grace period" for credential |
+/// expiration. This allows credential expiration checks to remain valid for a |
+/// reasonable amount of time. |
+const _EXPIRATION_GRACE = 10; |
+ |
+/// Handles a response from the authorization server that contains an access |
+/// token. This response format is common across several different components of |
+/// the OAuth2 flow. |
+Credentials handleAccessTokenResponse( |
+ http.Response response, |
+ Uri tokenEndpoint, |
+ Date startTime, |
+ List<String> scopes) { |
+ if (response.statusCode != 200) _handleErrorResponse(response, tokenEndpoint); |
+ |
+ void validate(bool condition, String message) => |
+ _validate(response, tokenEndpoint, condition, message); |
+ |
+ var contentType = response.headers['content-type']; |
+ if (contentType != null) { |
+ contentType = new ContentType.fromString(contentType); |
+ } |
+ validate(contentType != null && contentType.value == "application/json", |
+ 'content-type was "$contentType", expected "application/json"'); |
+ |
+ var parameters; |
+ try { |
+ parameters = JSON.parse(response.body); |
+ } catch (e) { |
+ validate(false, 'invalid JSON'); |
+ } |
+ |
+ for (var requiredParameter in ['access_token', 'token_type']) { |
+ validate(parameters.containsKey(requiredParameter), |
+ 'did not contain required parameter "$requiredParameter"'); |
+ validate(parameters[requiredParameter] is String, |
+ 'required parameter "$requiredParameter" was not a string, was ' |
+ '"${parameters[requiredParameter]}"'); |
+ } |
+ |
+ // TODO(nweiz): support the "mac" token type |
+ // (http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01) |
+ validate(parameters['token_type'].toLowerCase() == 'bearer', |
+ '"$tokenEndpoint": unknown token type "${parameters['token_type']}"'); |
+ |
+ var expiresIn = parameters['expires_in']; |
+ validate(expiresIn == null || expiresIn is int, |
+ 'parameter "expires_in" was not an int, was "$expiresIn"'); |
+ |
+ for (var name in ['refresh_token', 'scope']) { |
+ var value = parameters[name]; |
+ validate(value == null || value is String, |
+ 'parameter "$name" was not a string, was "$value"'); |
+ } |
+ |
+ var scope = parameters['scope']; |
+ if (scope != null) scopes = scope.split(" "); |
+ |
+ var expiration = expiresIn == null ? null : |
+ startTime.add(new Duration(seconds: expiresIn - _EXPIRATION_GRACE)); |
+ |
+ return new Credentials( |
+ parameters['access_token'], |
+ parameters['refresh_token'], |
+ tokenEndpoint, |
+ scopes, |
+ expiration); |
+} |
+ |
+/// Throws the appropriate exception for an error response from the |
+/// authorization server. |
+void _handleErrorResponse(http.Response response, Uri tokenEndpoint) { |
+ void validate(bool condition, String message) => |
+ _validate(response, tokenEndpoint, condition, message); |
+ |
+ // OAuth2 mandates a 400 or 401 response code for access token error |
+ // responses. If it's not a 400 reponse, the server is either broken or |
+ // off-spec. |
+ if (response.statusCode != 400 && response.statusCode != 401) { |
+ var reason = ''; |
+ if (response.reasonPhrase != null && !response.reasonPhrase.isEmpty) { |
+ ' ${response.reasonPhrase}'; |
+ } |
+ throw new FormatException('OAuth request for "$tokenEndpoint" failed ' |
+ 'with status ${response.statusCode}$reason.\n\n${response.body}'); |
+ } |
+ |
+ var contentType = response.headers['content-type']; |
+ if (contentType != null) { |
+ contentType = new ContentType.fromString(contentType); |
+ } |
+ validate(contentType != null && contentType.value == "application/json", |
+ 'content-type was "$contentType", expected "application/json"'); |
+ |
+ var parameters; |
+ try { |
+ parameters = JSON.parse(response.body); |
+ } catch (e) { |
+ validate(false, 'invalid JSON'); |
+ } |
+ |
+ validate(parameters.containsKey('error'), |
+ 'did not contain required parameter "error"'); |
+ validate(parameters["error"] is String, |
+ 'required parameter "error" was not a string, was ' |
+ '"${parameters["error"]}"'); |
+ |
+ for (var name in ['error_description', 'error_uri']) { |
+ var value = parameters[name]; |
+ validate(value == null || value is String, |
+ 'parameter "$name" was not a string, was "$value"'); |
+ } |
+ |
+ var description = parameters['error_description']; |
+ var uriString = parameters['error_uri']; |
+ var uri = uriString == null ? null : new Uri.fromString(uriString); |
+ throw new AuthorizationException(parameters['error'], description, uri); |
+} |
+ |
+void _validate( |
+ http.Response response, |
+ Uri tokenEndpoint, |
+ bool condition, |
+ String message) { |
+ if (condition) return; |
+ throw new FormatException('Invalid OAuth response for "$tokenEndpoint": ' |
+ '$message.\n\n${response.body}'); |
+} |