| OLD | NEW |
| 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import 'dart:convert'; | 5 import 'dart:convert'; |
| 6 | 6 |
| 7 import 'package:collection/collection.dart'; |
| 7 import 'package:http/http.dart' as http; | 8 import 'package:http/http.dart' as http; |
| 8 import 'package:http_parser/http_parser.dart'; | 9 import 'package:http_parser/http_parser.dart'; |
| 9 | 10 |
| 10 import 'credentials.dart'; | 11 import 'credentials.dart'; |
| 11 import 'authorization_exception.dart'; | 12 import 'authorization_exception.dart'; |
| 12 | 13 |
| 13 /// The amount of time to add as a "grace period" for credential expiration. | 14 /// The amount of time to add as a "grace period" for credential expiration. |
| 14 /// | 15 /// |
| 15 /// This allows credential expiration checks to remain valid for a reasonable | 16 /// This allows credential expiration checks to remain valid for a reasonable |
| 16 /// amount of time. | 17 /// amount of time. |
| 17 const _expirationGrace = const Duration(seconds: 10); | 18 const _expirationGrace = const Duration(seconds: 10); |
| 18 | 19 |
| 19 /// Handles a response from the authorization server that contains an access | 20 /// Handles a response from the authorization server that contains an access |
| 20 /// token. | 21 /// token. |
| 21 /// | 22 /// |
| 22 /// This response format is common across several different components of the | 23 /// This response format is common across several different components of the |
| 23 /// OAuth2 flow. | 24 /// OAuth2 flow. |
| 24 Credentials handleAccessTokenResponse( | 25 Credentials handleAccessTokenResponse( |
| 25 http.Response response, | 26 http.Response response, |
| 26 Uri tokenEndpoint, | 27 Uri tokenEndpoint, |
| 27 DateTime startTime, | 28 DateTime startTime, |
| 28 List<String> scopes) { | 29 List<String> scopes) { |
| 29 if (response.statusCode != 200) _handleErrorResponse(response, tokenEndpoint); | 30 if (response.statusCode != 200) _handleErrorResponse(response, tokenEndpoint); |
| 30 | 31 |
| 31 validate(condition, message) => | 32 validate(condition, message) => |
| 32 _validate(response, tokenEndpoint, condition, message); | 33 _validate(response, tokenEndpoint, condition, message); |
| 33 | 34 |
| 34 var contentType = response.headers['content-type']; | 35 var contentTypeString = response.headers['content-type']; |
| 35 if (contentType != null) contentType = new MediaType.parse(contentType); | 36 var contentType = contentTypeString == null |
| 37 ? null |
| 38 : new MediaType.parse(contentTypeString); |
| 36 | 39 |
| 37 // The spec requires a content-type of application/json, but some endpoints | 40 // The spec requires a content-type of application/json, but some endpoints |
| 38 // (e.g. Dropbox) serve it as text/javascript instead. | 41 // (e.g. Dropbox) serve it as text/javascript instead. |
| 39 validate(contentType != null && | 42 validate(contentType != null && |
| 40 (contentType.mimeType == "application/json" || | 43 (contentType.mimeType == "application/json" || |
| 41 contentType.mimeType == "text/javascript"), | 44 contentType.mimeType == "text/javascript"), |
| 42 'content-type was "$contentType", expected "application/json"'); | 45 'content-type was "$contentType", expected "application/json"'); |
| 43 | 46 |
| 44 var parameters; | 47 Map<String, dynamic> parameters; |
| 45 try { | 48 try { |
| 46 parameters = JSON.decode(response.body); | 49 var untypedParameters = JSON.decode(response.body); |
| 50 validate(untypedParameters is Map, |
| 51 'parameters must be a map, was "$parameters"'); |
| 52 parameters = DelegatingMap.typed(untypedParameters); |
| 47 } on FormatException catch (_) { | 53 } on FormatException catch (_) { |
| 48 validate(false, 'invalid JSON'); | 54 validate(false, 'invalid JSON'); |
| 49 } | 55 } |
| 50 | 56 |
| 51 for (var requiredParameter in ['access_token', 'token_type']) { | 57 for (var requiredParameter in ['access_token', 'token_type']) { |
| 52 validate(parameters.containsKey(requiredParameter), | 58 validate(parameters.containsKey(requiredParameter), |
| 53 'did not contain required parameter "$requiredParameter"'); | 59 'did not contain required parameter "$requiredParameter"'); |
| 54 validate(parameters[requiredParameter] is String, | 60 validate(parameters[requiredParameter] is String, |
| 55 'required parameter "$requiredParameter" was not a string, was ' | 61 'required parameter "$requiredParameter" was not a string, was ' |
| 56 '"${parameters[requiredParameter]}"'); | 62 '"${parameters[requiredParameter]}"'); |
| 57 } | 63 } |
| 58 | 64 |
| 59 // TODO(nweiz): support the "mac" token type | 65 // TODO(nweiz): support the "mac" token type |
| 60 // (http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01) | 66 // (http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01) |
| 61 validate(parameters['token_type'].toLowerCase() == 'bearer', | 67 validate(parameters['token_type'].toLowerCase() == 'bearer', |
| 62 '"$tokenEndpoint": unknown token type "${parameters['token_type']}"'); | 68 '"$tokenEndpoint": unknown token type "${parameters['token_type']}"'); |
| 63 | 69 |
| 64 var expiresIn = parameters['expires_in']; | 70 var expiresIn = parameters['expires_in']; |
| 65 validate(expiresIn == null || expiresIn is int, | 71 validate(expiresIn == null || expiresIn is int, |
| 66 'parameter "expires_in" was not an int, was "$expiresIn"'); | 72 'parameter "expires_in" was not an int, was "$expiresIn"'); |
| 67 | 73 |
| 68 for (var name in ['refresh_token', 'scope']) { | 74 for (var name in ['refresh_token', 'scope']) { |
| 69 var value = parameters[name]; | 75 var value = parameters[name]; |
| 70 validate(value == null || value is String, | 76 validate(value == null || value is String, |
| 71 'parameter "$name" was not a string, was "$value"'); | 77 'parameter "$name" was not a string, was "$value"'); |
| 72 } | 78 } |
| 73 | 79 |
| 74 var scope = parameters['scope']; | 80 var scope = parameters['scope'] as String; |
| 75 if (scope != null) scopes = scope.split(" "); | 81 if (scope != null) scopes = scope.split(" "); |
| 76 | 82 |
| 77 var expiration = expiresIn == null ? null : | 83 var expiration = expiresIn == null ? null : |
| 78 startTime.add(new Duration(seconds: expiresIn) - _expirationGrace); | 84 startTime.add(new Duration(seconds: expiresIn) - _expirationGrace); |
| 79 | 85 |
| 80 return new Credentials( | 86 return new Credentials( |
| 81 parameters['access_token'], | 87 parameters['access_token'], |
| 82 refreshToken: parameters['refresh_token'], | 88 refreshToken: parameters['refresh_token'], |
| 83 tokenEndpoint: tokenEndpoint, | 89 tokenEndpoint: tokenEndpoint, |
| 84 scopes: scopes, | 90 scopes: scopes, |
| (...skipping 11 matching lines...) Expand all Loading... |
| 96 // off-spec. | 102 // off-spec. |
| 97 if (response.statusCode != 400 && response.statusCode != 401) { | 103 if (response.statusCode != 400 && response.statusCode != 401) { |
| 98 var reason = ''; | 104 var reason = ''; |
| 99 if (response.reasonPhrase != null && !response.reasonPhrase.isEmpty) { | 105 if (response.reasonPhrase != null && !response.reasonPhrase.isEmpty) { |
| 100 ' ${response.reasonPhrase}'; | 106 ' ${response.reasonPhrase}'; |
| 101 } | 107 } |
| 102 throw new FormatException('OAuth request for "$tokenEndpoint" failed ' | 108 throw new FormatException('OAuth request for "$tokenEndpoint" failed ' |
| 103 'with status ${response.statusCode}$reason.\n\n${response.body}'); | 109 'with status ${response.statusCode}$reason.\n\n${response.body}'); |
| 104 } | 110 } |
| 105 | 111 |
| 106 var contentType = response.headers['content-type']; | 112 var contentTypeString = response.headers['content-type']; |
| 107 if (contentType != null) contentType = new MediaType.parse(contentType); | 113 var contentType = contentTypeString == null |
| 114 ? null |
| 115 : new MediaType.parse(contentTypeString); |
| 116 |
| 108 validate(contentType != null && contentType.mimeType == "application/json", | 117 validate(contentType != null && contentType.mimeType == "application/json", |
| 109 'content-type was "$contentType", expected "application/json"'); | 118 'content-type was "$contentType", expected "application/json"'); |
| 110 | 119 |
| 111 var parameters; | 120 var parameters; |
| 112 try { | 121 try { |
| 113 parameters = JSON.decode(response.body); | 122 parameters = JSON.decode(response.body); |
| 114 } on FormatException catch (_) { | 123 } on FormatException catch (_) { |
| 115 validate(false, 'invalid JSON'); | 124 validate(false, 'invalid JSON'); |
| 116 } | 125 } |
| 117 | 126 |
| (...skipping 17 matching lines...) Expand all Loading... |
| 135 | 144 |
| 136 void _validate( | 145 void _validate( |
| 137 http.Response response, | 146 http.Response response, |
| 138 Uri tokenEndpoint, | 147 Uri tokenEndpoint, |
| 139 bool condition, | 148 bool condition, |
| 140 String message) { | 149 String message) { |
| 141 if (condition) return; | 150 if (condition) return; |
| 142 throw new FormatException('Invalid OAuth response for "$tokenEndpoint": ' | 151 throw new FormatException('Invalid OAuth response for "$tokenEndpoint": ' |
| 143 '$message.\n\n${response.body}'); | 152 '$message.\n\n${response.body}'); |
| 144 } | 153 } |
| OLD | NEW |