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 |