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