Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(98)

Side by Side Diff: pkg/oauth2/lib/src/authorization_code_grant.dart

Issue 11420025: Add a package for authenticating via OAuth2. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Misc fixes Created 8 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698