OLD | NEW |
---|---|
(Empty) | |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library authorization_code_grant; | |
6 | |
7 import 'dart:uri'; | |
8 | |
9 import '../../../http/lib/http.dart' as http; | |
Bob Nystrom
2012/11/16 19:53:30
Add TODO to use "package:" here. Mention bug 6745.
nweiz
2012/11/17 01:06:27
Done.
| |
10 | |
11 import 'client.dart'; | |
12 import 'authorization_exception.dart'; | |
13 import 'handle_access_token_response.dart'; | |
14 import 'utils.dart'; | |
15 | |
16 /// A class for obtaining credentials via an [authorization code grant][]. This | |
17 /// method of authorization involves sending the resource owner to the | |
18 /// 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 /// used to obtain [Credentials] and create a fully-authorized [Client]. | |
21 /// | |
22 /// 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 /// back to your application, call [handleAuthorizationResponse] or | |
25 /// [handleAuthorizationCode] to process the authorization server's response and | |
26 /// construct a [Client]. | |
27 /// | |
28 /// [authorization code grant]: http://tools.ietf.org/html/draft-ietf-oauth-v2-3 1#section-4.1 | |
29 class AuthorizationCodeGrant { | |
Bob Nystrom
2012/11/16 19:53:30
The state enums and all of the StateErrors here ar
nweiz
2012/11/17 01:06:27
When I was designing this, I tried to strike a bal
Bob Nystrom
2012/11/19 21:37:10
SGTM.
| |
30 /// An enum value for [_state] indicating that [getAuthorizationUrl] has | |
Bob Nystrom
2012/11/16 19:53:30
Nittiest of nits, but it looks like most of these
nweiz
2012/11/17 01:06:27
Done.
| |
31 /// not yet been called for this grant. | |
32 static final int _INITIAL_STATE = 0; | |
Bob Nystrom
2012/11/16 19:53:30
"static final int" -> "static const". Here and els
nweiz
2012/11/17 01:06:27
Done.
What's the difference between "static const
Bob Nystrom
2012/11/19 21:37:10
"const" declares an actual Dart constant: somethin
| |
33 | |
34 // An enum value for [_state] indicating that [getAuthorizationUrl] has | |
35 // been called but neither [handleAuthorizationResponse] nor | |
36 // [handleAuthorizationCode] has been called. | |
37 static final int _AWAITING_RESPONSE_STATE = 1; | |
38 | |
39 // An enum value for [_state] indicating that [getAuthorizationUrl] and | |
40 // either [handleAuthorizationResponse] or [handleAuthorizationCode] have been | |
41 // called. | |
42 static final int _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 /// | |
49 /// This is usually global to the program using this library. | |
50 final String identifier; | |
51 | |
52 /// The client secret for this client. The authorization server will issue | |
53 /// each client a separate client identifier and secret, which allows the | |
54 /// server to tell which client is accessing it. Some servers may also have an | |
55 /// anonymous identifier/secret pair that any client may use. | |
56 /// | |
57 /// This is usually global to the program using this library. | |
58 /// | |
59 /// 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. | |
61 /// This is fine; OAuth2 servers generally won't rely on knowing with | |
62 /// certainty that a client is who it claims to be. | |
63 final String secret; | |
64 | |
65 /// 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 | |
67 /// client. This will usually be listed in the authorization server's | |
68 /// OAuth2 API documentation. | |
69 final Uri authorizationEndpoint; | |
70 | |
71 /// A URL provided by the authorization server that this library uses to | |
72 /// obtain long-lasting credentials. This will usually be listed in the | |
73 /// authorization server's OAuth2 API documentation. | |
74 final Uri tokenEndpoint; | |
75 | |
76 /// The HTTP client used to make HTTP requests. | |
77 final http.BaseClient _httpClient; | |
Bob Nystrom
2012/11/16 19:53:30
Using BaseClient as a type annotation here and els
nweiz
2012/11/17 01:06:27
I think this code gives a misleading impression ab
Bob Nystrom
2012/11/19 21:37:10
Yeah, I like this.
| |
78 | |
79 /// The URL to which the resource owner will be redirected after they | |
80 /// authorize this client with the authorization server. | |
81 Uri _redirectEndpoint; | |
82 | |
83 /// The scopes that the client is requesting access to. | |
84 List<String> _scopes; | |
85 | |
86 /// An opaque string that users of this library may specify that will be | |
87 /// included in the response query parameters. | |
88 String _stateString; | |
89 | |
90 /// The current state of the grant object. One of [_INITIAL_STATE], | |
91 /// [_AWAITING_RESPONSE_STATE], or [_FINISHED_STATE]. | |
92 int _state = _INITIAL_STATE; | |
93 | |
94 /// Creates a new grant. | |
95 /// | |
96 /// `httpClient` is used for all HTTP requests made by this grant, as well as | |
97 /// those of the [Client] is constructs. | |
98 AuthorizationCodeGrant( | |
99 this.identifier, | |
100 this.secret, | |
101 this.authorizationEndpoint, | |
102 this.tokenEndpoint, | |
103 {http.BaseClient httpClient}) | |
104 : _httpClient = httpClient == null ? new http.Client() : httpClient; | |
105 | |
106 /// Returns the URL to which the resource owner should be redirected to | |
Bob Nystrom
2012/11/16 19:53:30
"redirected" -> "directed"?
nweiz
2012/11/17 01:06:27
In most cases this will be run on a server, where
| |
107 /// authorize this client. The resource owner will then be redirected to | |
108 /// `redirect`, which should point to a server controlled by the client. This | |
Bob Nystrom
2012/11/16 19:53:30
backquotes -> square brackets. You can (and should
nweiz
2012/11/17 01:06:27
Done.
| |
109 /// redirect will have additional query parameters that should be passed to | |
110 /// [handleAuthorizationResponse]. | |
111 /// | |
112 /// The specific permissions being requested from the authorization server may | |
113 /// be specified via `scopes`. The scope strings are specific to the | |
114 /// 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 | |
116 /// [Credentials.scopes] field of [Client.credentials] to see which scopes you | |
117 /// were granted. | |
118 /// | |
119 /// An opaque `state` string may also be passed that will be present in the | |
120 /// query parameters provided to the redirect URL. | |
121 /// | |
122 /// It is a [StateError] to call this more than once for a given instance of | |
Bob Nystrom
2012/11/16 19:53:30
Remove the "for a given..." part. That should be i
nweiz
2012/11/17 01:06:27
Done.
| |
123 /// [AuthorizationCodeGrant]. | |
124 Uri getAuthorizationUrl(Uri redirect, | |
125 {List<String> scopes: const <String>[], String state}) { | |
126 if (_state != _INITIAL_STATE) { | |
127 throw new StateError('The authorization URL has already been generated.'); | |
128 } | |
129 _state = _AWAITING_RESPONSE_STATE; | |
130 | |
131 this._redirectEndpoint = redirect; | |
132 this._scopes = scopes; | |
133 this._stateString = state; | |
134 var parameters = { | |
135 "response_type": "code", | |
136 "client_id": this.identifier, | |
137 "redirect_uri": redirect.toString() | |
138 }; | |
139 | |
140 if (state != null) parameters['state'] = state; | |
141 if (!scopes.isEmpty) parameters['scope'] = Strings.join(scopes, ' '); | |
142 | |
143 return addQueryParameters(this.authorizationEndpoint, parameters); | |
144 } | |
145 | |
146 /// Processes the query parameters added to a redirect from the authorization | |
147 /// server. Note that this "response" is not an HTTP response, but rather the | |
Bob Nystrom
2012/11/16 19:53:30
Maybe: "...rather the data passed to the redirect
nweiz
2012/11/17 01:06:27
Done.
| |
148 /// query parameters added to the redirect URL. | |
149 /// | |
150 /// It is a [StateError] to call this more than once, to call it before | |
151 /// [getAuthorizationUrl] is called, or to call it after | |
152 /// [handleAuthorizationCode] is called. | |
153 /// | |
154 /// Throws [FormatError] if `parameters` is invalid according to the OAuth2 | |
155 /// spec or if the authorization server otherwise provides invalid responses. | |
156 /// If `state` was passed to [getAuthorizationUrl], this will throw a | |
157 /// [FormatError] if the `state` parameter doesn't match the original value. | |
158 /// | |
159 /// Throws [AuthorizationException] if the authorization fails. | |
160 Future<Client> handleAuthorizationResponse(Map<String, String> parameters) { | |
161 return async.chain((_) { | |
162 if (_state == _INITIAL_STATE) { | |
163 throw new StateError('The authorization URL has not yet been generated.' ); | |
Bob Nystrom
2012/11/16 19:53:30
Long line.
nweiz
2012/11/17 01:06:27
Done.
| |
164 } else if (_state == _FINISHED_STATE) { | |
165 throw new StateError('The authorization code has already been received.' ); | |
Bob Nystrom
2012/11/16 19:53:30
Ditto.
nweiz
2012/11/17 01:06:27
Done.
| |
166 } | |
167 _state = _FINISHED_STATE; | |
168 | |
169 if (this._stateString != null) { | |
Bob Nystrom
2012/11/16 19:53:30
Is the "this." needed here?
nweiz
2012/11/17 01:06:27
No. Removed.
| |
170 if (!parameters.containsKey('state')) { | |
171 throw new FormatException('Invalid OAuth response for ' | |
172 '"$authorizationEndpoint": did not contain required parameter ' | |
173 '"state".'); | |
Bob Nystrom
2012/11/16 19:53:30
How about:
'"$authorizationEndpoin
nweiz
2012/11/17 01:06:27
Done.
| |
174 } else if (parameters['state'] != this._stateString) { | |
Bob Nystrom
2012/11/16 19:53:30
"this."?
nweiz
2012/11/17 01:06:27
Done.
| |
175 throw new FormatException('Invalid OAuth response for ' | |
176 '"$authorizationEndpoint": parameter "state" expected to be ' | |
177 '"$_stateString", was "${parameters['state']}".'); | |
178 } | |
179 } | |
180 | |
181 if (parameters.containsKey('error')) { | |
182 var description = parameters['error_description']; | |
183 var uriString = parameters['error_uri']; | |
184 var uri = uriString == null ? null : new Uri.fromString(uriString); | |
185 throw new AuthorizationException(parameters['error'], description, uri); | |
186 } else if (!parameters.containsKey('code')) { | |
187 throw new FormatException('Invalid OAuth response for ' | |
188 '"$authorizationEndpoint": did not contain required parameter ' | |
189 '"code".'); | |
190 } | |
191 | |
192 return _handleAuthorizationCode(parameters['code']); | |
193 }); | |
194 } | |
195 | |
196 /// Processes an authorization code directly. Usually | |
197 /// [handleAuthorizationResponse] is preferable to this method, since it | |
198 /// validates all of the query parameters. However, some authorization servers | |
199 /// allow the user to copy and paste an authorization code into a command-line | |
200 /// application, in which case this method must be used. | |
201 /// | |
202 /// It is a [StateError] to call this more than once, to call it before | |
203 /// [getAuthorizationUrl] is called, or to call it after | |
204 /// [handleAuthorizationCode] is called. | |
205 /// | |
206 /// Throws [FormatError] if the authorization server provides invalid | |
207 /// responses while retrieving credentials. | |
208 /// | |
209 /// Throws [AuthorizationException] if the authorization fails. | |
210 Future<Client> handleAuthorizationCode(String authorizationCode) { | |
211 return async.chain((_) { | |
212 if (_state == _INITIAL_STATE) { | |
213 throw new StateError('The authorization URL has not yet been generated.' ); | |
Bob Nystrom
2012/11/16 19:53:30
Long lines.
nweiz
2012/11/17 01:06:27
Done.
| |
214 } else if (_state == _FINISHED_STATE) { | |
215 throw new StateError('The authorization code has already been received.' ); | |
216 } | |
217 _state = _FINISHED_STATE; | |
Bob Nystrom
2012/11/16 19:53:30
Move this into _handleAuthorizationCode so it's no
nweiz
2012/11/17 01:06:27
_handleAuthorizationCode exists explicitly to avoi
| |
218 | |
219 return _handleAuthorizationCode(authorizationCode); | |
220 }); | |
221 } | |
222 | |
223 /// This works just like [handleAuthorizationCode], except it doesn't validate | |
224 /// the state beforehand. | |
225 Future<Client> _handleAuthorizationCode(String authorizationCode) { | |
226 var startTime = new Date.now(); | |
227 return _httpClient.post(this.tokenEndpoint, fields: { | |
228 "grant_type": "authorization_code", | |
229 "code": authorizationCode, | |
230 "redirect_uri": this._redirectEndpoint.toString(), | |
231 // TODO(nweiz): the spec recommends that HTTP basic auth be used in | |
232 // preference to form parameters, but Google doesn't support that. Should | |
233 // it be configurable? | |
234 "client_id": this.identifier, | |
235 "client_secret": this.secret | |
236 }).transform((response) { | |
237 var credentials = handleAccessTokenResponse( | |
238 response, tokenEndpoint, startTime, _scopes); | |
239 return new Client( | |
240 this.identifier, this.secret, credentials, httpClient: _httpClient); | |
241 }); | |
242 } | |
243 | |
244 /// Closes the grant and frees its resources. | |
245 /// | |
246 /// This will close the underlying HTTP client, which is shared by the | |
247 /// [Client] created by this grant, so it's not safe to close the grant and | |
248 /// continue using the client. | |
Bob Nystrom
2012/11/16 19:53:30
Ugh, this makes me wonder if we should be ref-coun
nweiz
2012/11/17 01:06:27
I think it's fine as-is. This method isn't expecte
Adam
2012/11/17 07:29:21
_httpClient is final so setting to null should be
nweiz
2012/11/19 20:55:26
Good catch. This was also an issue in oauth2.Clien
| |
249 void close() { | |
250 if (_httpClient != null) _httpClient.close(); | |
251 _httpClient = null; | |
252 } | |
253 } | |
OLD | NEW |