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 library authorization_code_grant; | 5 library oauth2.authorization_code_grant; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 | 8 |
9 import 'package:http/http.dart' as http; | 9 import 'package:http/http.dart' as http; |
10 | 10 |
11 import 'client.dart'; | 11 import 'client.dart'; |
12 import 'authorization_exception.dart'; | 12 import 'authorization_exception.dart'; |
13 import 'handle_access_token_response.dart'; | 13 import 'handle_access_token_response.dart'; |
14 import 'utils.dart'; | 14 import 'utils.dart'; |
15 | 15 |
16 /// A class for obtaining credentials via an [authorization code grant][]. This | 16 /// A class for obtaining credentials via an [authorization code grant][]. |
17 /// method of authorization involves sending the resource owner to the | 17 /// |
18 /// This method of authorization involves sending the resource owner to the | |
18 /// authorization server where they will authorize the client. They're then | 19 /// authorization server where they will authorize the client. They're then |
19 /// redirected back to your server, along with an authorization code. This is | 20 /// redirected back to your server, along with an authorization code. This is |
20 /// used to obtain [Credentials] and create a fully-authorized [Client]. | 21 /// used to obtain [Credentials] and create a fully-authorized [Client]. |
21 /// | 22 /// |
22 /// To use this class, you must first call [getAuthorizationUrl] to get the URL | 23 /// To use this class, you must first call [getAuthorizationUrl] to get the URL |
23 /// to which to redirect the resource owner. Then once they've been redirected | 24 /// to which to redirect the resource owner. Then once they've been redirected |
24 /// back to your application, call [handleAuthorizationResponse] or | 25 /// back to your application, call [handleAuthorizationResponse] or |
25 /// [handleAuthorizationCode] to process the authorization server's response and | 26 /// [handleAuthorizationCode] to process the authorization server's response and |
26 /// construct a [Client]. | 27 /// construct a [Client]. |
27 /// | 28 /// |
28 /// [authorization code grant]: http://tools.ietf.org/html/draft-ietf-oauth-v2-3 1#section-4.1 | 29 /// [authorization code grant]: http://tools.ietf.org/html/draft-ietf-oauth-v2-3 1#section-4.1 |
29 class AuthorizationCodeGrant { | 30 class AuthorizationCodeGrant { |
30 /// An enum value for [_state] indicating that [getAuthorizationUrl] has not | 31 /// The client identifier for this client. |
31 /// yet been called for this grant. | 32 /// |
32 static const _INITIAL_STATE = 0; | 33 /// The authorization server will issue each client a separate client |
33 | 34 /// identifier and secret, which allows the server to tell which client is |
34 // An enum value for [_state] indicating that [getAuthorizationUrl] has been | 35 /// accessing it. Some servers may also have an anonymous identifier/secret |
35 // called but neither [handleAuthorizationResponse] nor | 36 /// pair that any client may use. |
36 // [handleAuthorizationCode] has been called. | |
37 static const _AWAITING_RESPONSE_STATE = 1; | |
38 | |
39 // An enum value for [_state] indicating that [getAuthorizationUrl] and either | |
40 // [handleAuthorizationResponse] or [handleAuthorizationCode] have been | |
41 // called. | |
42 static const _FINISHED_STATE = 2; | |
43 | |
44 /// The client identifier for this client. The authorization server will issue | |
45 /// each client a separate client identifier and secret, which allows the | |
46 /// server to tell which client is accessing it. Some servers may also have an | |
47 /// anonymous identifier/secret pair that any client may use. | |
48 /// | 37 /// |
49 /// This is usually global to the program using this library. | 38 /// This is usually global to the program using this library. |
50 final String identifier; | 39 final String identifier; |
51 | 40 |
52 /// The client secret for this client. The authorization server will issue | 41 /// The client secret for this client. |
53 /// each client a separate client identifier and secret, which allows the | 42 /// |
54 /// server to tell which client is accessing it. Some servers may also have an | 43 /// The authorization server will issue each client a separate client |
55 /// anonymous identifier/secret pair that any client may use. | 44 /// identifier and secret, which allows the server to tell which client is |
45 /// accessing it. Some servers may also have an anonymous identifier/secret | |
46 /// pair that any client may use. | |
56 /// | 47 /// |
57 /// This is usually global to the program using this library. | 48 /// This is usually global to the program using this library. |
58 /// | 49 /// |
59 /// Note that clients whose source code or binary executable is readily | 50 /// Note that clients whose source code or binary executable is readily |
60 /// available may not be able to make sure the client secret is kept a secret. | 51 /// available may not be able to make sure the client secret is kept a secret. |
61 /// This is fine; OAuth2 servers generally won't rely on knowing with | 52 /// This is fine; OAuth2 servers generally won't rely on knowing with |
62 /// certainty that a client is who it claims to be. | 53 /// certainty that a client is who it claims to be. |
63 final String secret; | 54 final String secret; |
64 | 55 |
65 /// A URL provided by the authorization server that serves as the base for the | 56 /// A URL provided by the authorization server that serves as the base for the |
66 /// URL that the resource owner will be redirected to to authorize this | 57 /// URL that the resource owner will be redirected to to authorize this |
67 /// client. This will usually be listed in the authorization server's | 58 /// client. |
68 /// OAuth2 API documentation. | 59 /// |
60 /// This will usually be listed in the authorization server's OAuth2 API | |
61 /// documentation. | |
69 final Uri authorizationEndpoint; | 62 final Uri authorizationEndpoint; |
70 | 63 |
71 /// A URL provided by the authorization server that this library uses to | 64 /// A URL provided by the authorization server that this library uses to |
72 /// obtain long-lasting credentials. This will usually be listed in the | 65 /// obtain long-lasting credentials. |
73 /// authorization server's OAuth2 API documentation. | 66 /// |
67 /// This will usually be listed in the authorization server's OAuth2 API | |
68 /// documentation. | |
74 final Uri tokenEndpoint; | 69 final Uri tokenEndpoint; |
75 | 70 |
76 /// The HTTP client used to make HTTP requests. | 71 /// The HTTP client used to make HTTP requests. |
77 http.Client _httpClient; | 72 http.Client _httpClient; |
78 | 73 |
79 /// The URL to which the resource owner will be redirected after they | 74 /// The URL to which the resource owner will be redirected after they |
80 /// authorize this client with the authorization server. | 75 /// authorize this client with the authorization server. |
81 Uri _redirectEndpoint; | 76 Uri _redirectEndpoint; |
82 | 77 |
83 /// The scopes that the client is requesting access to. | 78 /// The scopes that the client is requesting access to. |
84 List<String> _scopes; | 79 List<String> _scopes; |
85 | 80 |
86 /// An opaque string that users of this library may specify that will be | 81 /// An opaque string that users of this library may specify that will be |
87 /// included in the response query parameters. | 82 /// included in the response query parameters. |
88 String _stateString; | 83 String _stateString; |
89 | 84 |
90 /// The current state of the grant object. One of [_INITIAL_STATE], | 85 /// The current state of the grant object. |
91 /// [_AWAITING_RESPONSE_STATE], or [_FINISHED_STATE]. | 86 _State _state = _State.initial; |
92 int _state = _INITIAL_STATE; | |
93 | 87 |
94 /// Creates a new grant. | 88 /// Creates a new grant. |
95 /// | 89 /// |
96 /// [httpClient] is used for all HTTP requests made by this grant, as well as | 90 /// [httpClient] is used for all HTTP requests made by this grant, as well as |
97 /// those of the [Client] is constructs. | 91 /// those of the [Client] is constructs. |
98 AuthorizationCodeGrant( | 92 AuthorizationCodeGrant( |
99 this.identifier, | 93 this.identifier, |
100 this.secret, | 94 this.secret, |
101 this.authorizationEndpoint, | 95 this.authorizationEndpoint, |
102 this.tokenEndpoint, | 96 this.tokenEndpoint, |
103 {http.Client httpClient}) | 97 {http.Client httpClient}) |
104 : _httpClient = httpClient == null ? new http.Client() : httpClient; | 98 : _httpClient = httpClient == null ? new http.Client() : httpClient; |
105 | 99 |
106 /// Returns the URL to which the resource owner should be redirected to | 100 /// Returns the URL to which the resource owner should be redirected to |
107 /// authorize this client. The resource owner will then be redirected to | 101 /// authorize this client. |
108 /// [redirect], which should point to a server controlled by the client. This | 102 /// |
109 /// redirect will have additional query parameters that should be passed to | 103 /// The resource owner will then be redirected to [redirect], which should |
104 /// point to a server controlled by the client. This redirect will have | |
105 /// additional query parameters that should be passed to | |
110 /// [handleAuthorizationResponse]. | 106 /// [handleAuthorizationResponse]. |
111 /// | 107 /// |
112 /// The specific permissions being requested from the authorization server may | 108 /// The specific permissions being requested from the authorization server may |
113 /// be specified via [scopes]. The scope strings are specific to the | 109 /// be specified via [scopes]. The scope strings are specific to the |
114 /// authorization server and may be found in its documentation. Note that you | 110 /// authorization server and may be found in its documentation. Note that you |
115 /// may not be granted access to every scope you request; you may check the | 111 /// may not be granted access to every scope you request; you may check the |
116 /// [Credentials.scopes] field of [Client.credentials] to see which scopes you | 112 /// [Credentials.scopes] field of [Client.credentials] to see which scopes you |
117 /// were granted. | 113 /// were granted. |
118 /// | 114 /// |
119 /// An opaque [state] string may also be passed that will be present in the | 115 /// An opaque [state] string may also be passed that will be present in the |
120 /// query parameters provided to the redirect URL. | 116 /// query parameters provided to the redirect URL. |
121 /// | 117 /// |
122 /// It is a [StateError] to call this more than once. | 118 /// It is a [StateError] to call this more than once. |
123 Uri getAuthorizationUrl(Uri redirect, | 119 Uri getAuthorizationUrl(Uri redirect, |
124 {List<String> scopes: const <String>[], String state}) { | 120 {List<String> scopes: const <String>[], String state}) { |
125 if (_state != _INITIAL_STATE) { | 121 if (_state != _State.initial) { |
126 throw new StateError('The authorization URL has already been generated.'); | 122 throw new StateError('The authorization URL has already been generated.'); |
127 } | 123 } |
128 _state = _AWAITING_RESPONSE_STATE; | 124 _state = _State.awaitingResponse; |
129 | 125 |
130 this._redirectEndpoint = redirect; | 126 this._redirectEndpoint = redirect; |
131 this._scopes = scopes; | 127 this._scopes = scopes; |
132 this._stateString = state; | 128 this._stateString = state; |
133 var parameters = { | 129 var parameters = { |
134 "response_type": "code", | 130 "response_type": "code", |
135 "client_id": this.identifier, | 131 "client_id": this.identifier, |
136 "redirect_uri": redirect.toString() | 132 "redirect_uri": redirect.toString() |
137 }; | 133 }; |
138 | 134 |
139 if (state != null) parameters['state'] = state; | 135 if (state != null) parameters['state'] = state; |
140 if (!scopes.isEmpty) parameters['scope'] = scopes.join(' '); | 136 if (!scopes.isEmpty) parameters['scope'] = scopes.join(' '); |
141 | 137 |
142 return addQueryParameters(this.authorizationEndpoint, parameters); | 138 return addQueryParameters(this.authorizationEndpoint, parameters); |
143 } | 139 } |
144 | 140 |
145 /// Processes the query parameters added to a redirect from the authorization | 141 /// Processes the query parameters added to a redirect from the authorization |
146 /// server. Note that this "response" is not an HTTP response, but rather the | 142 /// server. |
147 /// data passed to a server controlled by the client as query parameters on | 143 /// |
148 /// the redirect URL. | 144 /// Note that this "response" is not an HTTP response, but rather the data |
145 /// passed to a server controlled by the client as query parameters on the | |
146 /// redirect URL. | |
149 /// | 147 /// |
150 /// It is a [StateError] to call this more than once, to call it before | 148 /// It is a [StateError] to call this more than once, to call it before |
151 /// [getAuthorizationUrl] is called, or to call it after | 149 /// [getAuthorizationUrl] is called, or to call it after |
152 /// [handleAuthorizationCode] is called. | 150 /// [handleAuthorizationCode] is called. |
153 /// | 151 /// |
154 /// Throws [FormatError] if [parameters] is invalid according to the OAuth2 | 152 /// Throws [FormatError] if [parameters] is invalid according to the OAuth2 |
155 /// spec or if the authorization server otherwise provides invalid responses. | 153 /// spec or if the authorization server otherwise provides invalid responses. |
156 /// If `state` was passed to [getAuthorizationUrl], this will throw a | 154 /// If `state` was passed to [getAuthorizationUrl], this will throw a |
157 /// [FormatError] if the `state` parameter doesn't match the original value. | 155 /// [FormatError] if the `state` parameter doesn't match the original value. |
158 /// | 156 /// |
159 /// Throws [AuthorizationException] if the authorization fails. | 157 /// Throws [AuthorizationException] if the authorization fails. |
160 Future<Client> handleAuthorizationResponse(Map<String, String> parameters) | 158 Future<Client> handleAuthorizationResponse(Map<String, String> parameters) |
161 async { | 159 async { |
162 if (_state == _INITIAL_STATE) { | 160 if (_state == _State.initial) { |
163 throw new StateError( | 161 throw new StateError( |
164 'The authorization URL has not yet been generated.'); | 162 'The authorization URL has not yet been generated.'); |
165 } else if (_state == _FINISHED_STATE) { | 163 } else if (_state == _State.finished) { |
166 throw new StateError( | 164 throw new StateError( |
167 'The authorization code has already been received.'); | 165 'The authorization code has already been received.'); |
168 } | 166 } |
169 _state = _FINISHED_STATE; | 167 _state = _State.finished; |
170 | 168 |
171 if (_stateString != null) { | 169 if (_stateString != null) { |
172 if (!parameters.containsKey('state')) { | 170 if (!parameters.containsKey('state')) { |
173 throw new FormatException('Invalid OAuth response for ' | 171 throw new FormatException('Invalid OAuth response for ' |
174 '"$authorizationEndpoint": parameter "state" expected to be ' | 172 '"$authorizationEndpoint": parameter "state" expected to be ' |
175 '"$_stateString", was missing.'); | 173 '"$_stateString", was missing.'); |
176 } else if (parameters['state'] != _stateString) { | 174 } else if (parameters['state'] != _stateString) { |
177 throw new FormatException('Invalid OAuth response for ' | 175 throw new FormatException('Invalid OAuth response for ' |
178 '"$authorizationEndpoint": parameter "state" expected to be ' | 176 '"$authorizationEndpoint": parameter "state" expected to be ' |
179 '"$_stateString", was "${parameters['state']}".'); | 177 '"$_stateString", was "${parameters['state']}".'); |
180 } | 178 } |
181 } | 179 } |
182 | 180 |
183 if (parameters.containsKey('error')) { | 181 if (parameters.containsKey('error')) { |
184 var description = parameters['error_description']; | 182 var description = parameters['error_description']; |
185 var uriString = parameters['error_uri']; | 183 var uriString = parameters['error_uri']; |
186 var uri = uriString == null ? null : Uri.parse(uriString); | 184 var uri = uriString == null ? null : Uri.parse(uriString); |
187 throw new AuthorizationException(parameters['error'], description, uri); | 185 throw new AuthorizationException(parameters['error'], description, uri); |
188 } else if (!parameters.containsKey('code')) { | 186 } else if (!parameters.containsKey('code')) { |
189 throw new FormatException('Invalid OAuth response for ' | 187 throw new FormatException('Invalid OAuth response for ' |
190 '"$authorizationEndpoint": did not contain required parameter ' | 188 '"$authorizationEndpoint": did not contain required parameter ' |
191 '"code".'); | 189 '"code".'); |
192 } | 190 } |
193 | 191 |
194 return await _handleAuthorizationCode(parameters['code']); | 192 return await _handleAuthorizationCode(parameters['code']); |
195 } | 193 } |
196 | 194 |
197 /// Processes an authorization code directly. Usually | 195 /// Processes an authorization code directly. |
198 /// [handleAuthorizationResponse] is preferable to this method, since it | 196 /// |
199 /// validates all of the query parameters. However, some authorization servers | 197 /// Usually [handleAuthorizationResponse] is preferable to this method, since |
200 /// allow the user to copy and paste an authorization code into a command-line | 198 /// it validates all of the query parameters. However, some authorization |
201 /// application, in which case this method must be used. | 199 /// servers allow the user to copy and paste an authorization code into a |
200 /// command-line application, in which case this method must be used. | |
202 /// | 201 /// |
203 /// It is a [StateError] to call this more than once, to call it before | 202 /// It is a [StateError] to call this more than once, to call it before |
204 /// [getAuthorizationUrl] is called, or to call it after | 203 /// [getAuthorizationUrl] is called, or to call it after |
205 /// [handleAuthorizationCode] is called. | 204 /// [handleAuthorizationCode] is called. |
206 /// | 205 /// |
207 /// Throws [FormatError] if the authorization server provides invalid | 206 /// Throws [FormatError] if the authorization server provides invalid |
208 /// responses while retrieving credentials. | 207 /// responses while retrieving credentials. |
209 /// | 208 /// |
210 /// Throws [AuthorizationException] if the authorization fails. | 209 /// Throws [AuthorizationException] if the authorization fails. |
211 Future<Client> handleAuthorizationCode(String authorizationCode) async { | 210 Future<Client> handleAuthorizationCode(String authorizationCode) async { |
212 if (_state == _INITIAL_STATE) { | 211 if (_state == _State.initial) { |
213 throw new StateError( | 212 throw new StateError( |
214 'The authorization URL has not yet been generated.'); | 213 'The authorization URL has not yet been generated.'); |
215 } else if (_state == _FINISHED_STATE) { | 214 } else if (_state == _State.finished) { |
216 throw new StateError( | 215 throw new StateError( |
217 'The authorization code has already been received.'); | 216 'The authorization code has already been received.'); |
218 } | 217 } |
219 _state = _FINISHED_STATE; | 218 _state = _State.finished; |
220 | 219 |
221 return await _handleAuthorizationCode(authorizationCode); | 220 return await _handleAuthorizationCode(authorizationCode); |
222 } | 221 } |
223 | 222 |
224 /// This works just like [handleAuthorizationCode], except it doesn't validate | 223 /// This works just like [handleAuthorizationCode], except it doesn't validate |
225 /// the state beforehand. | 224 /// the state beforehand. |
226 Future<Client> _handleAuthorizationCode(String authorizationCode) async { | 225 Future<Client> _handleAuthorizationCode(String authorizationCode) async { |
227 var startTime = new DateTime.now(); | 226 var startTime = new DateTime.now(); |
228 var response = await _httpClient.post(this.tokenEndpoint, body: { | 227 var response = await _httpClient.post(this.tokenEndpoint, body: { |
229 "grant_type": "authorization_code", | 228 "grant_type": "authorization_code", |
(...skipping 15 matching lines...) Expand all Loading... | |
245 /// Closes the grant and frees its resources. | 244 /// Closes the grant and frees its resources. |
246 /// | 245 /// |
247 /// This will close the underlying HTTP client, which is shared by the | 246 /// This will close the underlying HTTP client, which is shared by the |
248 /// [Client] created by this grant, so it's not safe to close the grant and | 247 /// [Client] created by this grant, so it's not safe to close the grant and |
249 /// continue using the client. | 248 /// continue using the client. |
250 void close() { | 249 void close() { |
251 if (_httpClient != null) _httpClient.close(); | 250 if (_httpClient != null) _httpClient.close(); |
252 _httpClient = null; | 251 _httpClient = null; |
253 } | 252 } |
254 } | 253 } |
254 | |
255 /// States that [AuthorizationCodeGrant] can be in. | |
256 class _State { | |
Bob Nystrom
2015/08/25 21:49:10
I think you might actually be able to use an enum
nweiz
2015/08/25 22:06:54
Technically yes, but it makes debugging prints rea
| |
257 /// [AuthorizationCodeGrant.getAuthorizationUrl] has not yet been called for | |
258 /// this grant. | |
259 static const initial = const _State("initial"); | |
260 | |
261 // [AuthorizationCodeGrant.getAuthorizationUrl] has been called but neither | |
262 // [AuthorizationCodeGrant.handleAuthorizationResponse] nor | |
263 // [AuthorizationCodeGrant.handleAuthorizationCode] has been called. | |
264 static const awaitingResponse = const _State("awaiting response"); | |
265 | |
266 // [AuthorizationCodeGrant.getAuthorizationUrl] and either | |
267 // [AuthorizationCodeGrant.handleAuthorizationResponse] or | |
268 // [AuthorizationCodeGrant.handleAuthorizationCode] have been called. | |
269 static const finished = const _State("finished"); | |
270 | |
271 final String _name; | |
272 | |
273 const _State(this._name); | |
274 | |
275 String toString() => _name; | |
276 } | |
OLD | NEW |