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 credentials; |
| 6 |
| 7 import 'dart:json'; |
| 8 import 'dart:uri'; |
| 9 |
| 10 import '../../../http/lib/http.dart' as http; |
| 11 import 'handle_access_token_response.dart'; |
| 12 import 'utils.dart'; |
| 13 |
| 14 /// Credentials that prove that a client is allowed to access a resource on the |
| 15 /// resource owner's behalf. These credentials are long-lasting and can be |
| 16 /// safely persisted across multiple runs of the program. |
| 17 /// |
| 18 /// Many authorization servers will attach an expiration date to a set of |
| 19 /// credentials, along with a token that can be used to refresh the credentials |
| 20 /// once they've expired. The [Client] will automatically refresh its |
| 21 /// credentials when necessary. It's also possible to explicitly refresh them |
| 22 /// via [Client.refreshCredentials] or [Credentials.refresh]. |
| 23 /// |
| 24 /// Note that a given set of credentials can only be refreshed once, so be sure |
| 25 /// to save the refreshed credentials for future use. |
| 26 class Credentials { |
| 27 /// The token that is sent to the resource server to prove the authorization |
| 28 /// of a client. |
| 29 final String accessToken; |
| 30 |
| 31 /// The token that is sent to the authorization server to refresh the |
| 32 /// credentials. This is optional. |
| 33 final String refreshToken; |
| 34 |
| 35 /// The URL of the authorization server endpoint that's used to refresh the |
| 36 /// credentials. This is optional. |
| 37 final Uri tokenEndpoint; |
| 38 |
| 39 /// The specific permissions being requested from the authorization server. |
| 40 /// The scope strings are specific to the authorization server and may be |
| 41 /// found in its documentation. |
| 42 final List<String> scopes; |
| 43 |
| 44 /// The date at which these credentials will expire. This is likely to be a |
| 45 /// few seconds earlier than the server's idea of the expiration date. |
| 46 final Date expiration; |
| 47 |
| 48 /// Whether or not these credentials have expired. Note that it's possible the |
| 49 /// credentials will expire shortly after this is called. However, since the |
| 50 /// client's expiration date is kept a few seconds earlier than the server's, |
| 51 /// there should be enough leeway to rely on this. |
| 52 bool get isExpired => expiration != null && new Date.now() > expiration; |
| 53 |
| 54 /// Whether it's possible to refresh these credentials. |
| 55 bool get canRefresh => refreshToken != null && tokenEndpoint != null; |
| 56 |
| 57 /// Creates a new set of credentials. |
| 58 /// |
| 59 /// This class is usually not constructed directly; rather, it's accessed via |
| 60 /// [Client.credentials] after a [Client] is created by |
| 61 /// [AuthorizationCodeGrant]. Alternately, it may be loaded from a serialized |
| 62 /// form via [Credentials.fromJson]. |
| 63 Credentials( |
| 64 this.accessToken, |
| 65 [this.refreshToken, |
| 66 this.tokenEndpoint, |
| 67 this.scopes, |
| 68 this.expiration]); |
| 69 |
| 70 /// Loads a set of credentials from a JSON-serialized form. Throws |
| 71 /// [FormatException] if the JSON is incorrectly formatted. |
| 72 factory Credentials.fromJson(String json) { |
| 73 void validate(bool condition, String message) { |
| 74 if (condition) return; |
| 75 throw new FormatException( |
| 76 "Failed to load credentials: $message.\n\n$json"); |
| 77 } |
| 78 |
| 79 var parsed; |
| 80 try { |
| 81 parsed = JSON.parse(json); |
| 82 } catch (e) { |
| 83 validate(false, 'invalid JSON'); |
| 84 } |
| 85 |
| 86 validate(parsed is Map, 'was not a JSON map'); |
| 87 validate(parsed.containsKey('accessToken'), |
| 88 'did not contain required field "accessToken"'); |
| 89 validate(parsed['accessToken'] is String, |
| 90 'required field "accessToken" was not a string, was ' |
| 91 '${parsed["accessToken"]}'); |
| 92 |
| 93 |
| 94 for (var stringField in ['refreshToken', 'tokenEndpoint']) { |
| 95 var value = parsed[stringField]; |
| 96 validate(value == null || value is String, |
| 97 'field "$stringField" was not a string, was "$value"'); |
| 98 } |
| 99 |
| 100 var scopes = parsed['scopes']; |
| 101 validate(scopes == null || scopes is List, |
| 102 'field "scopes" was not a list, was "$scopes"'); |
| 103 |
| 104 var tokenEndpoint = parsed['tokenEndpoint']; |
| 105 if (tokenEndpoint != null) { |
| 106 tokenEndpoint = new Uri.fromString(tokenEndpoint); |
| 107 } |
| 108 var expiration = parsed['expiration']; |
| 109 if (expiration != null) { |
| 110 validate(expiration is int, |
| 111 'field "expiration" was not an int, was "$expiration"'); |
| 112 expiration = new Date.fromMillisecondsSinceEpoch(expiration); |
| 113 } |
| 114 |
| 115 return new Credentials( |
| 116 parsed['accessToken'], |
| 117 parsed['refreshToken'], |
| 118 tokenEndpoint, |
| 119 scopes, |
| 120 expiration); |
| 121 } |
| 122 |
| 123 /// Serializes a set of credentials to JSON. Nothing is guaranteed about the |
| 124 /// output except that it's valid JSON and compatible with |
| 125 /// [Credentials.toJson]. |
| 126 String toJson() => JSON.stringify({ |
| 127 'accessToken': accessToken, |
| 128 'refreshToken': refreshToken, |
| 129 'tokenEndpoint': tokenEndpoint == null ? null : tokenEndpoint.toString(), |
| 130 'scopes': scopes, |
| 131 'expiration': expiration == null ? null : expiration.millisecondsSinceEpoch |
| 132 }); |
| 133 |
| 134 /// Returns a new set of refreshed credentials. See [Client.identifier] and |
| 135 /// [Client.secret] for explanations of those parameters. |
| 136 /// |
| 137 /// You may request different scopes than the default by passing in |
| 138 /// [newScopes]. These must be a subset of [scopes]. |
| 139 /// |
| 140 /// This will throw a [StateError] if these credentials can't be refreshed, an |
| 141 /// [AuthorizationException] if refreshing the credentials fails, or a |
| 142 /// [FormatError] if the authorization server returns invalid responses. |
| 143 Future<Credentials> refresh( |
| 144 String identifier, |
| 145 String secret, |
| 146 {List<String> newScopes, |
| 147 http.BaseClient httpClient}) { |
| 148 var scopes = this.scopes; |
| 149 if (newScopes != null) scopes = newScopes; |
| 150 if (scopes == null) scopes = <String>[]; |
| 151 if (httpClient == null) httpClient = new http.Client(); |
| 152 |
| 153 var startTime = new Date.now(); |
| 154 return async.chain((_) { |
| 155 if (refreshToken == null) { |
| 156 throw new StateError("Can't refresh credentials without a refresh " |
| 157 "token."); |
| 158 } else if (tokenEndpoint == null) { |
| 159 throw new StateError("Can't refresh credentials without a token " |
| 160 "endpoint."); |
| 161 } |
| 162 |
| 163 var fields = { |
| 164 "grant_type": "refresh_token", |
| 165 "refresh_token": refreshToken, |
| 166 // TODO(nweiz): the spec recommends that HTTP basic auth be used in |
| 167 // preference to form parameters, but Google doesn't support that. |
| 168 // Should it be configurable? |
| 169 "client_id": identifier, |
| 170 "client_secret": secret |
| 171 }; |
| 172 if (!scopes.isEmpty) fields["scope"] = Strings.join(scopes, ' '); |
| 173 |
| 174 return httpClient.post(tokenEndpoint, fields: fields); |
| 175 }).transform((response) { |
| 176 return handleAccessTokenResponse( |
| 177 response, tokenEndpoint, startTime, scopes); |
| 178 }).transform((credentials) { |
| 179 // The authorization server may issue a new refresh token. If it doesn't, |
| 180 // we should re-use the one we already have. |
| 181 if (credentials.refreshToken != null) return credentials; |
| 182 return new Credentials( |
| 183 credentials.accessToken, |
| 184 this.refreshToken, |
| 185 credentials.tokenEndpoint, |
| 186 credentials.scopes, |
| 187 credentials.expiration); |
| 188 }); |
| 189 } |
| 190 } |
OLD | NEW |