├── .gitignore ├── .status ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── lib ├── auth.dart ├── auth_browser.dart ├── auth_io.dart └── src │ ├── adc_utils.dart │ ├── auth_http_utils.dart │ ├── crypto │ ├── asn1.dart │ ├── pem.dart │ ├── rsa.dart │ └── rsa_sign.dart │ ├── http_client_base.dart │ ├── oauth2_flows │ ├── auth_code.dart │ ├── implicit.dart │ ├── jwt.dart │ └── metadata_server.dart │ ├── typedefs.dart │ └── utils.dart ├── pubspec.yaml └── test ├── adc_test.dart ├── crypto ├── asn1_test.dart ├── pem_test.dart ├── rsa_sign_test.dart └── rsa_test.dart ├── http_client_base_test.dart ├── oauth2_flows ├── auth_code_test.dart ├── implicit │ ├── gapi_auth_hybrid_force.js │ ├── gapi_auth_hybrid_force_test.dart │ ├── gapi_auth_hybrid_immediate.js │ ├── gapi_auth_hybrid_immediate_test.dart │ ├── gapi_auth_hybrid_nonforce.js │ ├── gapi_auth_hybrid_nonforce_test.dart │ ├── gapi_auth_immediate.js │ ├── gapi_auth_immediate_test.dart │ ├── gapi_auth_implicit_idtoken.js │ ├── gapi_auth_implicit_idtoken_test.dart │ ├── gapi_auth_nonforce.js │ ├── gapi_auth_nonforce_test.dart │ ├── gapi_auth_user_denied.js │ ├── gapi_auth_user_denied_test.dart │ ├── gapi_initialize_failure.js │ ├── gapi_initialize_failure_test.dart │ ├── gapi_initialize_successful.js │ ├── gapi_initialize_successful_test.dart │ ├── gapi_load_failure.js │ ├── gapi_load_failure_test.dart │ └── utils.dart ├── jwt_test.dart └── metadata_server_test.dart ├── oauth2_test.dart └── test_utils.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .packages 2 | .pub 3 | .dart_tool/ 4 | packages 5 | pubspec.lock 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.status: -------------------------------------------------------------------------------- 1 | */packages/*: Skip 2 | */*/packages/*: Skip 3 | */*/*/packages/*: Skip 4 | 5 | [ $runtime == vm ] 6 | test/oauth2_flows/implicit/*: Skip # Uses dart:html 7 | build/test/oauth2_flows/implicit/*: Skip # Uses dart:html 8 | 9 | [ $runtime != vm ] 10 | test/oauth2_flows/auth_code_test: Skip # Uses dart:io 11 | build/test/oauth2_flows/auth_code_test: Skip # Uses dart:io 12 | 13 | [ $compiler == dart2js ] 14 | test/crypto/pem_test: Skip # Big integer 15 | test/crypto/rsa_sign_test: Skip # Big integer 16 | test/crypto/rsa_sign_test: Skip # Big integer 17 | test/crypto/rsa_test: Skip # Big integer 18 | test/oauth2_flows/jwt_test: Skip # Big integer 19 | test/oauth2_test: Skip # Big integer 20 | 21 | build/test/crypto/pem_test: Skip # Big integer 22 | build/test/crypto/rsa_sign_test: Skip # Big integer 23 | build/test/crypto/rsa_sign_test: Skip # Big integer 24 | build/test/crypto/rsa_test: Skip # Big integer 25 | build/test/oauth2_flows/jwt_test: Skip # Big integer 26 | build/test/oauth2_test: Skip # Big integer 27 | 28 | [ $compiler == dart2js && ($runtime == d8 || $runtime == jsshell) ] 29 | *: Skip 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: dart 2 | 3 | dart: 4 | - dev 5 | 6 | dart_task: 7 | - dartanalyzer 8 | - dartfmt 9 | - test: --platform vm 10 | - test: --platform firefox,chrome -j 1 11 | 12 | # Only building master means that we don't run two builds for each pull request. 13 | branches: 14 | only: [master] 15 | 16 | cache: 17 | directories: 18 | - $HOME/.pub-cache 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.12+2-dev 2 | 3 | ## 0.2.12+1 4 | 5 | * Removed a `dart:async` import that isn't required for \>=Dart 2.1. 6 | * Require \>=Dart 2.1. 7 | 8 | ## 0.2.12 9 | * Add `clientViaApplicationDefaultCredentials` for obtaining credentials using 10 | [ADC](https://cloud.google.com/docs/authentication/production). 11 | 12 | ## 0.2.11+1 13 | * Fix 'multiple completer completion' bug in `ImplicitFlow`. 14 | 15 | ## 0.2.11 16 | * Add the `force` parameter to the `obtainAccessCredentialsViaUserConsent` API. 17 | 18 | ## 0.2.10 19 | * Look for GCE metadata host in environment under `$GCE_METADATA_HOST`. 20 | 21 | ## 0.2.9 22 | * Prepare for [Uint8List SDK breaking change](Prepare for Uint8List SDK breaking change). 23 | 24 | ## 0.2.8 25 | 26 | * Initialize implicit browser flows statically, allowing multiple ImplicitFlow 27 | objects to initialize without trying to load the gapi JavaScript library 28 | multiple times. 29 | 30 | ## 0.2.7 31 | 32 | - Support for specifying desired `ResponseType`, allowing applications to 33 | obtain an `id_token` using `ImplicitBrowserFlow`. 34 | 35 | ## 0.2.6 36 | 37 | - Ignore script loading error after timeout for in-browser implicit login-flow. 38 | 39 | ## 0.2.5+3 40 | 41 | - Support `package:http` `>=0.11.3+17 <0.13.0`. 42 | 43 | ## 0.2.5+2 44 | 45 | * Support Dart 2. 46 | 47 | ## 0.2.5+1 48 | 49 | * Switch all uppercase constants from `dart:convert` to lowercase. 50 | 51 | ## 0.2.5 52 | 53 | * Add an optional `loginHint` parameter to browser oauth2 flow APIs which can be 54 | used to specify a hint as to which user is being logged in. 55 | 56 | ## 0.2.4 57 | 58 | * Added `id_token` to `AccessCredentials` 59 | 60 | * Migrated to Dart 2 `BigInt`. 61 | 62 | ## 0.2.3+6 63 | 64 | - Fix async issue in oauth2 flow implementation 65 | 66 | ## 0.2.3+5 67 | 68 | - Support the latest version of `crypto` package. 69 | 70 | ## 0.2.3+4 71 | 72 | - Make package strong-mode compliant. 73 | 74 | ## 0.2.3+3 75 | 76 | - Support package:crypto >= 0.9.2 77 | 78 | ## 0.2.3+2 79 | 80 | - Use preferred "Metadata-Flavor" HTTP header in 81 | `MetadataServerAuthorizationFlow` instead of the deprecated 82 | "X-Google-Metadata-Request" header. 83 | 84 | ## 0.2.3 85 | 86 | - Allow `ServiceAccountCredentials` constructors to take an optional 87 | `user` argument to specify a user to impersonate. 88 | 89 | ## 0.2.2 90 | 91 | - Allow `ServiceAccountCredentials.fromJson` to accept a `Map`. 92 | - Cleaned up `README.md` 93 | 94 | ## 0.2.1 95 | - Added optional `force` and `immediate` arguments to `runHybridFlow`. 96 | 97 | ## 0.2.0 98 | - Renamed `forceUserConsent` parameter to `immediate`. 99 | - Added `runHybridFlow` function to `auth_browser`, with corresponding 100 | `HybridFlowResult` class. 101 | 102 | ## 0.1.1 103 | - Add `clientViaApiKey` functions to `auth_io` ad `auth_browser`. 104 | 105 | ## 0.1.0 106 | - First release. 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014, the Dart project authors. All rights reserved. 2 | Redistribution and use in source and binary forms, with or without 3 | modification, are permitted provided that the following conditions are 4 | met: 5 | 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above 9 | copyright notice, this list of conditions and the following 10 | disclaimer in the documentation and/or other materials provided 11 | with the distribution. 12 | * Neither the name of Google Inc. nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo has been archived 2 | The source for googleapis_auth is now at https://github.com/dart-lang/googleapis/tree/master/googleapis_auth 3 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | linter: 2 | rules: 3 | # Errors 4 | - avoid_empty_else 5 | - control_flow_in_finally 6 | - empty_statements 7 | - test_types_in_equals 8 | - throw_in_finally 9 | - valid_regexps 10 | 11 | # Style 12 | #- annotate_overrides 13 | - avoid_init_to_null 14 | - avoid_return_types_on_setters 15 | - await_only_futures 16 | - camel_case_types 17 | - comment_references 18 | - empty_catches 19 | - empty_constructor_bodies 20 | - hash_and_equals 21 | - library_prefixes 22 | - non_constant_identifier_names 23 | - prefer_is_not_empty 24 | - slash_for_doc_comments 25 | - type_init_formals 26 | - unrelated_type_equality_checks 27 | -------------------------------------------------------------------------------- /lib/auth.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.auth; 6 | 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | 10 | import 'package:http/http.dart'; 11 | 12 | import 'src/auth_http_utils.dart'; 13 | import 'src/crypto/pem.dart'; 14 | import 'src/crypto/rsa.dart'; 15 | import 'src/http_client_base.dart'; 16 | import 'src/utils.dart'; 17 | 18 | /// An OAuth2 access token. 19 | class AccessToken { 20 | /// The token type, usually "Bearer" 21 | final String type; 22 | 23 | /// The access token data. 24 | final String data; 25 | 26 | /// Time at which the token will be expired (UTC time) 27 | final DateTime expiry; 28 | 29 | /// [expiry] must be a UTC `DateTime`. 30 | AccessToken(this.type, this.data, this.expiry) { 31 | if (type == null || data == null || expiry == null) { 32 | throw new ArgumentError('Arguments type/data/expiry may not be null.'); 33 | } 34 | 35 | if (!expiry.isUtc) { 36 | throw new ArgumentError('The expiry date must be a Utc DateTime.'); 37 | } 38 | } 39 | 40 | bool get hasExpired { 41 | return new DateTime.now().toUtc().isAfter(expiry); 42 | } 43 | 44 | String toString() => "AccessToken(type=$type, data=$data, expiry=$expiry)"; 45 | } 46 | 47 | /// OAuth2 Credentials. 48 | class AccessCredentials { 49 | /// An access token. 50 | final AccessToken accessToken; 51 | 52 | /// A refresh token, which can be used to refresh the access credentials. 53 | final String? refreshToken; 54 | 55 | /// A JWT used in calls to Google APIs that accept an id_token param. 56 | final String? idToken; 57 | 58 | /// Scopes these credentials are valid for. 59 | final List scopes; 60 | 61 | AccessCredentials(this.accessToken, this.refreshToken, this.scopes, 62 | {this.idToken}) { 63 | if (accessToken == null || scopes == null) { 64 | throw new ArgumentError('Arguments accessToken/scopes must not be null.'); 65 | } 66 | } 67 | } 68 | 69 | /// Represents the client application's credentials. 70 | class ClientId { 71 | /// The identifier used to identify this application to the server. 72 | final String identifier; 73 | 74 | /// The client secret used to identify this application to the server. 75 | final String? secret; 76 | 77 | ClientId(this.identifier, this.secret) { 78 | if (identifier == null) { 79 | throw new ArgumentError('Argument identifier may not be null.'); 80 | } 81 | } 82 | 83 | ClientId.serviceAccount(this.identifier) : secret = null { 84 | if (identifier == null) { 85 | throw new ArgumentError('Argument identifier may not be null.'); 86 | } 87 | } 88 | } 89 | 90 | /// Represents credentials for a service account. 91 | class ServiceAccountCredentials { 92 | /// The email address of this service account. 93 | final String email; 94 | 95 | /// The clientId. 96 | final ClientId clientId; 97 | 98 | /// Private key. 99 | final String privateKey; 100 | 101 | /// Impersonated user, if any. If not impersonating any user this is `null`. 102 | final String? impersonatedUser; 103 | 104 | /// Private key as an [RSAPrivateKey]. 105 | final RSAPrivateKey privateRSAKey; 106 | 107 | /// Creates a new [ServiceAccountCredentials] from JSON. 108 | /// 109 | /// [json] can be either a [Map] or a JSON map encoded as a [String]. 110 | /// 111 | /// The optional named argument [impersonatedUser] is used to set the user 112 | /// to impersonate if impersonating a user. 113 | factory ServiceAccountCredentials.fromJson(json, {String? impersonatedUser}) { 114 | if (json is String) { 115 | json = jsonDecode(json); 116 | } 117 | if (json is! Map) { 118 | throw new ArgumentError('json must be a Map or a String encoding a Map.'); 119 | } 120 | var identifier = json['client_id']; 121 | var privateKey = json['private_key']; 122 | var email = json['client_email']; 123 | var type = json['type']; 124 | 125 | if (type != 'service_account') { 126 | throw new ArgumentError('The given credentials are not of type ' 127 | 'service_account (was: $type).'); 128 | } 129 | 130 | if (identifier == null || privateKey == null || email == null) { 131 | throw new ArgumentError('The given credentials do not contain all the ' 132 | 'fields: client_id, private_key and client_email.'); 133 | } 134 | 135 | var clientId = new ClientId(identifier, null); 136 | return new ServiceAccountCredentials(email, clientId, privateKey, 137 | impersonatedUser: impersonatedUser); 138 | } 139 | 140 | /// Creates a new [ServiceAccountCredentials]. 141 | /// 142 | /// [email] is the e-mail address of the service account. 143 | /// 144 | /// [clientId] is the client ID for the service account. 145 | /// 146 | /// [privateKey] is the base 64 encoded, unencrypted private key, including 147 | /// the '-----BEGIN PRIVATE KEY-----' and '-----END PRIVATE KEY-----' 148 | /// boundaries. 149 | /// 150 | /// The optional named argument [impersonatedUser] is used to set the user 151 | /// to impersonate if impersonating a user is needed. 152 | ServiceAccountCredentials(this.email, this.clientId, String privateKey, 153 | {this.impersonatedUser}) 154 | : privateKey = privateKey, 155 | privateRSAKey = keyFromString(privateKey) { 156 | if (email == null || clientId == null || privateKey == null) { 157 | throw new ArgumentError( 158 | 'Arguments email/clientId/privateKey must not be null.'); 159 | } 160 | } 161 | } 162 | 163 | /// A authenticated HTTP client. 164 | abstract class AuthClient implements Client { 165 | /// The credentials currently used for making HTTP requests. 166 | AccessCredentials get credentials; 167 | } 168 | 169 | /// A autorefreshing, authenticated HTTP client. 170 | abstract class AutoRefreshingAuthClient implements AuthClient { 171 | /// A broadcast stream of [AccessCredentials]. 172 | /// 173 | /// A listener will get notified when new [AccessCredentials] were obtained. 174 | Stream get credentialUpdates; 175 | } 176 | 177 | /// Thrown if an attempt to refresh a token failed. 178 | class RefreshFailedException implements Exception { 179 | final String message; 180 | RefreshFailedException(this.message); 181 | String toString() => message; 182 | } 183 | 184 | /// Thrown if an attempt to make an authorized request failed. 185 | class AccessDeniedException implements Exception { 186 | final String message; 187 | AccessDeniedException(this.message); 188 | String toString() => message; 189 | } 190 | 191 | /// Thrown if user did not give his consent. 192 | class UserConsentException implements Exception { 193 | final String message; 194 | UserConsentException(this.message); 195 | String toString() => message; 196 | } 197 | 198 | /// Obtain an `http.Client` which automatically authenticates 199 | /// requests using [credentials]. 200 | /// 201 | /// Note that the returned `RequestHandler` will not auto-refresh the given 202 | /// [credentials]. 203 | /// 204 | /// The user is responsible for closing the returned HTTP [Client]. 205 | /// Closing the returned [Client] will not close [baseClient]. 206 | AuthClient authenticatedClient( 207 | Client baseClient, AccessCredentials credentials) { 208 | if (credentials.accessToken.type != 'Bearer') { 209 | throw new ArgumentError('Only Bearer access tokens are accepted.'); 210 | } 211 | return new AuthenticatedClient(baseClient, credentials); 212 | } 213 | 214 | /// Obtain an `http.Client` which automatically refreshes [credentials] 215 | /// before they expire. Uses [baseClient] as a base for making authenticated 216 | /// http requests and for refreshing [credentials]. 217 | /// 218 | /// The user is responsible for closing the returned HTTP [Client]. 219 | /// Closing the returned [Client] will not close [baseClient]. 220 | AutoRefreshingAuthClient autoRefreshingClient( 221 | ClientId clientId, AccessCredentials credentials, Client baseClient) { 222 | if (credentials.accessToken.type != 'Bearer') { 223 | throw new ArgumentError('Only Bearer access tokens are accepted.'); 224 | } 225 | if (credentials.refreshToken == null) { 226 | throw new ArgumentError('Refresh token in AccessCredentials was `null`.'); 227 | } 228 | return new AutoRefreshingClient(baseClient, clientId, credentials); 229 | } 230 | 231 | /// Tries to obtain refreshed [AccessCredentials] based on [credentials] using 232 | /// [client]. 233 | Future refreshCredentials( 234 | ClientId clientId, AccessCredentials credentials, Client client) async { 235 | var formValues = [ 236 | 'client_id=${Uri.encodeComponent(clientId.identifier)}', 237 | 'client_secret=${Uri.encodeComponent(clientId.secret!)}', 238 | 'refresh_token=${Uri.encodeComponent(credentials.refreshToken!)}', 239 | 'grant_type=refresh_token', 240 | ]; 241 | 242 | var body = new Stream>.fromIterable( 243 | [(ascii.encode(formValues.join('&')))]); 244 | var request = new RequestImpl('POST', _googleTokenUri, body); 245 | request.headers['content-type'] = 'application/x-www-form-urlencoded'; 246 | 247 | var response = await client.send(request); 248 | var contentType = response.headers['content-type']; 249 | contentType = contentType == null ? null : contentType.toLowerCase(); 250 | 251 | if (contentType == null || 252 | (!contentType.contains('json') && !contentType.contains('javascript'))) { 253 | await response.stream.drain().catchError((_) {}); 254 | throw new Exception( 255 | 'Server responded with invalid content type: $contentType. ' 256 | 'Expected json response.'); 257 | } 258 | 259 | var jsonMap = await response.stream 260 | .transform(ascii.decoder) 261 | .transform(json.decoder) 262 | .first as Map; 263 | 264 | var idToken = jsonMap['id_token']; 265 | var token = jsonMap['access_token']; 266 | var seconds = jsonMap['expires_in']; 267 | var tokenType = jsonMap['token_type']; 268 | var error = jsonMap['error']; 269 | 270 | if (response.statusCode != 200 && error != null) { 271 | throw new RefreshFailedException('Refreshing attempt failed. ' 272 | 'Response was ${response.statusCode}. Error message was $error.'); 273 | } 274 | 275 | if (token == null || seconds is! int || tokenType != 'Bearer') { 276 | throw new Exception('Refresing attempt failed. ' 277 | 'Invalid server response.'); 278 | } 279 | 280 | return new AccessCredentials( 281 | new AccessToken(tokenType, token, expiryDate(seconds)), 282 | credentials.refreshToken, 283 | credentials.scopes, 284 | idToken: idToken); 285 | } 286 | 287 | /// Available response types that can be requested when using the implicit 288 | /// browser login flow. 289 | /// 290 | /// More information about these values can be found here: 291 | /// https://developers.google.com/identity/protocols/OpenIDConnect#response-type 292 | enum ResponseType { 293 | /// Requests an access code. This triggers the basic rather than the implicit 294 | /// flow. 295 | code, 296 | 297 | /// Requests the user's identity token when running the implicit flow. 298 | idToken, 299 | 300 | /// Requests the user's current permissions. 301 | permission, 302 | 303 | /// Requests the user's access token when running the implicit flow. 304 | token, 305 | } 306 | 307 | final _googleTokenUri = Uri.parse('https://accounts.google.com/o/oauth2/token'); 308 | -------------------------------------------------------------------------------- /lib/auth_browser.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.auth_browser; 6 | 7 | import 'dart:async'; 8 | 9 | import 'package:http/browser_client.dart'; 10 | import 'package:http/http.dart'; 11 | 12 | import 'auth.dart'; 13 | import 'src/auth_http_utils.dart'; 14 | import 'src/http_client_base.dart'; 15 | import 'src/oauth2_flows/implicit.dart'; 16 | 17 | export 'auth.dart'; 18 | 19 | /// Obtains a HTTP client which uses the given [apiKey] for making HTTP 20 | /// requests. 21 | /// 22 | /// Note that the returned client should *only* be used for making HTTP requests 23 | /// to Google Services. The [apiKey] should not be disclosed to third parties. 24 | /// 25 | /// The user is responsible for closing the returned HTTP [Client]. 26 | /// Closing the returned [Client] will not close [baseClient]. 27 | Client clientViaApiKey(String apiKey, {Client? baseClient}) { 28 | if (baseClient == null) { 29 | baseClient = new BrowserClient(); 30 | } else { 31 | baseClient = nonClosingClient(baseClient); 32 | } 33 | return new ApiKeyClient(baseClient, apiKey); 34 | } 35 | 36 | /// Will create and complete with a [BrowserOAuth2Flow] object. 37 | /// 38 | /// This function will perform an implicit browser based oauth2 flow. 39 | /// 40 | /// It will load Google's `gapi` library and initialize it. After initialization 41 | /// it will complete with a [BrowserOAuth2Flow] object. The flow object can be 42 | /// used to obtain `AccessCredentials` or an authenticated HTTP client. 43 | /// 44 | /// If loading or initializing the `gapi` library results in an error, this 45 | /// future will complete with an error. 46 | /// 47 | /// If [baseClient] is not given, one will be automatically created. It will be 48 | /// used for making authenticated HTTP requests. See [BrowserOAuth2Flow]. 49 | /// 50 | /// The [ClientId] can be obtained in the Google Cloud Console. 51 | /// 52 | /// The user is responsible for closing the returned [BrowserOAuth2Flow] object. 53 | /// Closing the returned [BrowserOAuth2Flow] will not close [baseClient] 54 | /// if one was given. 55 | Future createImplicitBrowserFlow( 56 | ClientId clientId, List scopes, 57 | {Client? baseClient}) { 58 | var refCountedClient = baseClient == null 59 | ? RefCountedClient(BrowserClient(), initialRefCount: 1) 60 | : RefCountedClient(baseClient, initialRefCount: 2); 61 | 62 | var flow = new ImplicitFlow(clientId.identifier, scopes); 63 | return flow.initialize().catchError((error, stack) { 64 | refCountedClient.close(); 65 | return new Future.error(error, stack); 66 | }).then((_) => new BrowserOAuth2Flow._(flow, refCountedClient)); 67 | } 68 | 69 | /// Used for obtaining oauth2 access credentials. 70 | /// 71 | /// Warning: 72 | /// 73 | /// The methods `obtainAccessCredentialsViaUserConsent` and 74 | /// `clientViaUserConsent` try to open a popup window for the user authorization 75 | /// dialog. 76 | /// 77 | /// In order to prevent browsers from blocking the popup window, these 78 | /// methods should only be called inside an event handler, since most 79 | /// browsers do not block popup windows created in response to a user 80 | /// interaction. 81 | class BrowserOAuth2Flow { 82 | final ImplicitFlow _flow; 83 | final RefCountedClient _client; 84 | 85 | bool _wasClosed = false; 86 | 87 | /// The HTTP client passed in will be closed if `close` was called and all 88 | /// generated HTTP clients via [clientViaUserConsent] were closed. 89 | BrowserOAuth2Flow._(this._flow, this._client); 90 | 91 | /// Obtain oauth2 [AccessCredentials]. 92 | /// 93 | /// If [immediate] is `true` there will be no user involvement. If the user 94 | /// is either not logged in or has not already granted the application access, 95 | /// a `UserConsentException` will be thrown. 96 | /// 97 | /// If [immediate] is `false` the user might be asked to login (if he is not 98 | /// already logged in) and might get asked to grant the application access 99 | /// (if the application hasn't been granted access before). 100 | /// 101 | /// If [force] is `true` this will create a popup window and ask the user to 102 | /// grant the application offline access. In case the user is not already 103 | /// logged in, he will be presented with an login dialog first. 104 | /// 105 | /// If [force] is `false` this will only create a popup window if the user 106 | /// has not already granted the application access. 107 | /// 108 | /// If [loginHint] is not `null`, it will be passed to the server as a hint 109 | /// to which user is being signed-in. This can e.g. be an email or a User ID 110 | /// which might be used as pre-selection in the sign-in flow. 111 | /// 112 | /// If [responseTypes] is not `null` or empty, it will be sent to the server 113 | /// to inform the server of the type of responses to reply with. 114 | /// 115 | /// The returned future will complete with `AccessCredentials` if the user 116 | /// has given the application access to it's data. Otherwise the future will 117 | /// complete with a `UserConsentException`. 118 | /// 119 | /// In case another error occurs the returned future will complete with an 120 | /// `Exception`. 121 | Future obtainAccessCredentialsViaUserConsent( 122 | {bool immediate: false, 123 | bool force: false, 124 | String? loginHint, 125 | List? responseTypes}) { 126 | _ensureOpen(); 127 | return _flow.login( 128 | force: force, 129 | immediate: immediate, 130 | loginHint: loginHint, 131 | responseTypes: responseTypes); 132 | } 133 | 134 | /// Obtains [AccessCredentials] and returns an authenticated HTTP client. 135 | /// 136 | /// After obtaining access credentials, this function will return an HTTP 137 | /// [Client]. HTTP requests made on the returned client will get an 138 | /// additional `Authorization` header with the `AccessCredentials` obtained. 139 | /// 140 | /// In case the `AccessCredentials` expire, it will try to obtain new ones 141 | /// without user consent. 142 | /// 143 | /// See [obtainAccessCredentialsViaUserConsent] for how credentials will be 144 | /// obtained. Errors from [obtainAccessCredentialsViaUserConsent] will be let 145 | /// through to the returned `Future` of this function and to the returned 146 | /// HTTP client (in case of credential refreshes). 147 | /// 148 | /// The returned HTTP client will forward errors from lower levels via it's 149 | /// `Future` or it's `Response.read()` stream. 150 | /// 151 | /// The user is responsible for closing the returned HTTP client. 152 | Future clientViaUserConsent( 153 | {bool immediate: false, String? loginHint}) { 154 | return obtainAccessCredentialsViaUserConsent( 155 | immediate: immediate, loginHint: loginHint) 156 | .then(_clientFromCredentials); 157 | } 158 | 159 | /// Obtains [AccessCredentials] and an authorization code which can be 160 | /// exchanged for permanent access credentials. 161 | /// 162 | /// Use case: 163 | /// A web application might want to get consent for accessing data on behalf 164 | /// of a user. The client part is a dynamic webapp which wants to open a 165 | /// popup which asks the user for consent. The webapp might want to use the 166 | /// credentials to make API calls, but the server may want to have offline 167 | /// access to user data as well. 168 | /// 169 | /// If [force] is `true` this will create a popup window and ask the user to 170 | /// grant the application offline access. In case the user is not already 171 | /// logged in, he will be presented with an login dialog first. 172 | /// 173 | /// If [force] is `false` this will only create a popup window if the user 174 | /// has not already granted the application access. Please note that the 175 | /// authorization code can only be exchanged for a refresh token if the user 176 | /// had to grant access via the popup window. Otherwise the code exchange 177 | /// will only give an access token. 178 | /// 179 | /// If [loginHint] is not `null`, it will be passed to the server as a hint 180 | /// to which user is being signed-in. This can e.g. be an email or a User ID 181 | /// which might be used as pre-selection in the sign-in flow. 182 | /// 183 | /// If [immediate] is `true` there will be no user involvement. If the user 184 | /// is either not logged in or has not already granted the application access, 185 | /// a `UserConsentException` will be thrown. 186 | Future runHybridFlow( 187 | {bool force: true, bool immediate: false, String? loginHint}) async { 188 | _ensureOpen(); 189 | var result = await _flow.loginHybrid( 190 | force: force, immediate: immediate, loginHint: loginHint); 191 | return new HybridFlowResult(this, result.credential, result.code); 192 | } 193 | 194 | /// Will close this [BrowserOAuth2Flow] object and the HTTP [Client] it is 195 | /// using. 196 | /// 197 | /// The clients obtained via [clientViaUserConsent] will continue to work. 198 | /// The client obtained via `newClient` of obtained [HybridFlowResult] objects 199 | /// will continue to work. 200 | /// 201 | /// After this flow object and all obtained clients were closed the underlying 202 | /// HTTP client will be closed as well. 203 | /// 204 | /// After calling this `close` method, calls to [clientViaUserConsent], 205 | /// [obtainAccessCredentialsViaUserConsent] and to `newClient` on returned 206 | /// [HybridFlowResult] objects will fail. 207 | void close() { 208 | _ensureOpen(); 209 | _wasClosed = true; 210 | _client.close(); 211 | } 212 | 213 | void _ensureOpen() { 214 | if (_wasClosed) { 215 | throw new StateError('BrowserOAuth2Flow has already been closed.'); 216 | } 217 | } 218 | 219 | AutoRefreshingAuthClient _clientFromCredentials(AccessCredentials cred) { 220 | _ensureOpen(); 221 | _client.acquire(); 222 | return new _AutoRefreshingBrowserClient(_client, cred, _flow); 223 | } 224 | } 225 | 226 | /// Represents the result of running a browser based hybrid flow. 227 | /// 228 | /// The `credentials` field holds credentials which can be used on the client 229 | /// side. The `newClient` function can be used to make a new authenticated HTTP 230 | /// client using these credentials. 231 | /// 232 | /// The `authorizationCode` can be sent to the server, which knows the 233 | /// "client secret" and can exchange it with long-lived access credentials. 234 | /// 235 | /// See the `obtainAccessCredentialsViaCodeExchange` function in the 236 | /// `googleapis_auth.auth_io` library for more details on how to use the 237 | /// authorization code. 238 | class HybridFlowResult { 239 | final BrowserOAuth2Flow _flow; 240 | 241 | /// Access credentials for making authenticated HTTP requests. 242 | final AccessCredentials credentials; 243 | 244 | /// The authorization code received from the authorization endpoint. 245 | /// 246 | /// The auth code can be used to receive permanent access credentials. 247 | /// This requires a confidential client which can keep a secret. 248 | final String? authorizationCode; 249 | 250 | HybridFlowResult(this._flow, this.credentials, this.authorizationCode); 251 | 252 | AutoRefreshingAuthClient newClient() { 253 | _flow._ensureOpen(); 254 | return _flow._clientFromCredentials(credentials); 255 | } 256 | } 257 | 258 | class _AutoRefreshingBrowserClient extends AutoRefreshDelegatingClient { 259 | AccessCredentials credentials; 260 | ImplicitFlow _flow; 261 | Client _authClient; 262 | 263 | _AutoRefreshingBrowserClient(Client client, this.credentials, this._flow) 264 | : _authClient = authenticatedClient(client, credentials), 265 | super(client); 266 | 267 | Future send(BaseRequest request) { 268 | if (!credentials.accessToken.hasExpired) { 269 | return _authClient.send(request); 270 | } else { 271 | return _flow.login(immediate: true).then((newCredentials) { 272 | credentials = newCredentials; 273 | notifyAboutNewCredentials(credentials); 274 | _authClient = authenticatedClient(baseClient, credentials); 275 | return _authClient.send(request); 276 | }); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /lib/auth_io.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.auth_io; 6 | 7 | import 'dart:io'; 8 | 9 | import 'package:http/http.dart'; 10 | 11 | import 'auth.dart'; 12 | import 'src/auth_http_utils.dart'; 13 | import 'src/adc_utils.dart'; 14 | import 'src/http_client_base.dart'; 15 | import 'src/oauth2_flows/auth_code.dart'; 16 | import 'src/oauth2_flows/jwt.dart'; 17 | import 'src/oauth2_flows/metadata_server.dart'; 18 | import 'src/typedefs.dart'; 19 | 20 | export 'auth.dart'; 21 | export 'src/typedefs.dart'; 22 | 23 | /// Create a client using [Application Default Credentials][ADC]. 24 | /// 25 | /// Looks for credentials in the following order of preference: 26 | /// 1. A JSON file whose path is specified by `GOOGLE_APPLICATION_CREDENTIALS`, 27 | /// this file typically contains [exported service account keys][svc-keys]. 28 | /// 2. A JSON file created by [`gcloud auth application-default login`][gcloud-login] 29 | /// in a well-known location (`%APPDATA%/gcloud/application_default_credentials.json` 30 | /// on Windows and `$HOME/.config/gcloud/application_default_credentials.json` on Linux/Mac). 31 | /// 3. On Google Compute Engine and App Engine Flex we fetch credentials from 32 | /// [GCE metadata service][metadata]. 33 | /// 34 | /// [metadata]: https://cloud.google.com/compute/docs/storing-retrieving-metadata 35 | /// [svc-keys]: https://cloud.google.com/docs/authentication/getting-started 36 | /// [gcloud-login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login 37 | /// [ADC]: https://cloud.google.com/docs/authentication/production 38 | Future clientViaApplicationDefaultCredentials({ 39 | required List scopes, 40 | Client? baseClient, 41 | }) async { 42 | if (baseClient == null) { 43 | baseClient = new Client(); 44 | } else { 45 | baseClient = nonClosingClient(baseClient); 46 | } 47 | 48 | // If env var specifies a file to load credentials from we'll do that. 49 | final credsEnv = Platform.environment['GOOGLE_APPLICATION_CREDENTIALS']; 50 | if (credsEnv != null && credsEnv.isNotEmpty) { 51 | // If env var is specific and not empty, we always try to load, even if 52 | // the file doesn't exist. 53 | return await fromApplicationsCredentialsFile( 54 | File(credsEnv), 55 | 'GOOGLE_APPLICATION_CREDENTIALS', 56 | scopes, 57 | baseClient, 58 | ); 59 | } 60 | 61 | // Attempt to use file created by `gcloud auth application-default login` 62 | File credFile; 63 | if (Platform.isWindows) { 64 | credFile = File.fromUri(Uri.directory(Platform.environment['APPDATA']!) 65 | .resolve('gcloud/application_default_credentials.json')); 66 | } else { 67 | credFile = File.fromUri(Uri.directory(Platform.environment['HOME']!) 68 | .resolve('.config/gcloud/application_default_credentials.json')); 69 | } 70 | // Only try to load from credFile if it exists. 71 | if (await credFile.exists()) { 72 | return await fromApplicationsCredentialsFile( 73 | credFile, 74 | '`gcloud auth application-default login`', 75 | scopes, 76 | baseClient, 77 | ); 78 | } 79 | 80 | return await clientViaMetadataServer(baseClient: baseClient); 81 | } 82 | 83 | /// Obtains oauth2 credentials and returns an authenticated HTTP client. 84 | /// 85 | /// See [obtainAccessCredentialsViaUserConsent] for specifics about the 86 | /// arguments used for obtaining access credentials. 87 | /// 88 | /// Once access credentials have been obtained, this function will complete 89 | /// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire 90 | /// it will use it's refresh token (if available) to obtain new credentials. 91 | /// See [autoRefreshingClient] for more information. 92 | /// 93 | /// If [baseClient] is not given, one will be automatically created. It will be 94 | /// used for making authenticated HTTP requests. 95 | /// 96 | /// The user is responsible for closing the returned HTTP [Client]. 97 | /// Closing the returned [Client] will not close [baseClient]. 98 | Future clientViaUserConsent( 99 | ClientId clientId, List scopes, PromptUserForConsent userPrompt, 100 | {Client? baseClient}) async { 101 | bool closeUnderlyingClient = false; 102 | if (baseClient == null) { 103 | baseClient = new Client(); 104 | closeUnderlyingClient = true; 105 | } 106 | 107 | var flow = new AuthorizationCodeGrantServerFlow( 108 | clientId, scopes, baseClient, userPrompt); 109 | 110 | AccessCredentials credentials; 111 | 112 | try { 113 | credentials = await flow.run(); 114 | } catch (e) { 115 | if (closeUnderlyingClient) { 116 | baseClient.close(); 117 | } 118 | rethrow; 119 | } 120 | return new AutoRefreshingClient(baseClient, clientId, credentials, 121 | closeUnderlyingClient: closeUnderlyingClient); 122 | } 123 | 124 | /// Obtains oauth2 credentials and returns an authenticated HTTP client. 125 | /// 126 | /// See [obtainAccessCredentialsViaUserConsentManual] for specifics about the 127 | /// arguments used for obtaining access credentials. 128 | /// 129 | /// Once access credentials have been obtained, this function will complete 130 | /// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire 131 | /// it will use it's refresh token (if available) to obtain new credentials. 132 | /// See [autoRefreshingClient] for more information. 133 | /// 134 | /// If [baseClient] is not given, one will be automatically created. It will be 135 | /// used for making authenticated HTTP requests. 136 | /// 137 | /// The user is responsible for closing the returned HTTP [Client]. 138 | /// Closing the returned [Client] will not close [baseClient]. 139 | Future clientViaUserConsentManual(ClientId clientId, 140 | List scopes, PromptUserForConsentManual userPrompt, 141 | {Client? baseClient}) async { 142 | bool closeUnderlyingClient = false; 143 | if (baseClient == null) { 144 | baseClient = new Client(); 145 | closeUnderlyingClient = true; 146 | } 147 | 148 | var flow = new AuthorizationCodeGrantManualFlow( 149 | clientId, scopes, baseClient, userPrompt); 150 | 151 | AccessCredentials credentials; 152 | 153 | try { 154 | credentials = await flow.run(); 155 | } catch (e) { 156 | if (closeUnderlyingClient) { 157 | baseClient.close(); 158 | } 159 | rethrow; 160 | } 161 | 162 | return new AutoRefreshingClient(baseClient, clientId, credentials, 163 | closeUnderlyingClient: closeUnderlyingClient); 164 | } 165 | 166 | /// Obtains oauth2 credentials and returns an authenticated HTTP client. 167 | /// 168 | /// See [obtainAccessCredentialsViaServiceAccount] for specifics about the 169 | /// arguments used for obtaining access credentials. 170 | /// 171 | /// Once access credentials have been obtained, this function will complete 172 | /// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire 173 | /// it will obtain new access credentials. 174 | /// 175 | /// If [baseClient] is not given, one will be automatically created. It will be 176 | /// used for making authenticated HTTP requests and for obtaining access 177 | /// credentials. 178 | /// 179 | /// The user is responsible for closing the returned HTTP [Client]. 180 | /// Closing the returned [Client] will not close [baseClient]. 181 | Future clientViaServiceAccount( 182 | ServiceAccountCredentials clientCredentials, List scopes, 183 | {Client? baseClient}) async { 184 | if (baseClient == null) { 185 | baseClient = new Client(); 186 | } else { 187 | baseClient = nonClosingClient(baseClient); 188 | } 189 | 190 | var flow = new JwtFlow( 191 | clientCredentials.email, 192 | clientCredentials.privateRSAKey, 193 | clientCredentials.impersonatedUser, 194 | scopes, 195 | baseClient); 196 | 197 | AccessCredentials credentials; 198 | try { 199 | credentials = await flow.run(); 200 | } catch (e) { 201 | baseClient.close(); 202 | rethrow; 203 | } 204 | 205 | return new _ServiceAccountClient(baseClient, credentials, flow); 206 | } 207 | 208 | /// Obtains oauth2 credentials and returns an authenticated HTTP client. 209 | /// 210 | /// See [obtainAccessCredentialsViaMetadataServer] for specifics about the 211 | /// arguments used for obtaining access credentials. 212 | /// 213 | /// Once access credentials have been obtained, this function will complete 214 | /// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire 215 | /// it will obtain new access credentials. 216 | /// 217 | /// If [baseClient] is not given, one will be automatically created. It will be 218 | /// used for making authenticated HTTP requests and for obtaining access 219 | /// credentials. 220 | /// 221 | /// The user is responsible for closing the returned HTTP [Client]. 222 | /// Closing the returned [Client] will not close [baseClient]. 223 | Future clientViaMetadataServer( 224 | {Client? baseClient}) async { 225 | if (baseClient == null) { 226 | baseClient = new Client(); 227 | } else { 228 | baseClient = nonClosingClient(baseClient); 229 | } 230 | 231 | var flow = new MetadataServerAuthorizationFlow(baseClient); 232 | 233 | AccessCredentials credentials; 234 | 235 | try { 236 | credentials = await flow.run(); 237 | } catch (e) { 238 | baseClient.close(); 239 | rethrow; 240 | } 241 | return new _MetadataServerClient(baseClient, credentials, flow); 242 | } 243 | 244 | /// Obtains a HTTP client which uses the given [apiKey] for making HTTP 245 | /// requests. 246 | /// 247 | /// Note that the returned client should *only* be used for making HTTP requests 248 | /// to Google Services. The [apiKey] should not be disclosed to third parties. 249 | /// 250 | /// The user is responsible for closing the returned HTTP [Client]. 251 | /// Closing the returned [Client] will not close [baseClient]. 252 | Client clientViaApiKey(String apiKey, {Client? baseClient}) { 253 | if (baseClient == null) { 254 | baseClient = new Client(); 255 | } else { 256 | baseClient = nonClosingClient(baseClient); 257 | } 258 | return new ApiKeyClient(baseClient, apiKey); 259 | } 260 | 261 | /// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow. 262 | /// 263 | /// The returned future will complete with `AccessCredentials` if the user 264 | /// has given the application access to it's data. Otherwise the future will 265 | /// complete with a `UserConsentException`. 266 | /// 267 | /// In case another error occurs the returned future will complete with an 268 | /// `Exception`. 269 | /// 270 | /// [userPrompt] will be used for directing the user/user-agent to a URI. See 271 | /// [PromptUserForConsent] for more information. 272 | /// 273 | /// [client] will be used for obtaining `AccessCredentials` from an 274 | /// authorization code. 275 | /// 276 | /// The [ClientId] can be obtained in the Google Cloud Console. 277 | Future obtainAccessCredentialsViaUserConsent( 278 | ClientId clientId, 279 | List scopes, 280 | Client client, 281 | PromptUserForConsent userPrompt) { 282 | return new AuthorizationCodeGrantServerFlow( 283 | clientId, scopes, client, userPrompt) 284 | .run(); 285 | } 286 | 287 | /// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow. 288 | /// 289 | /// The returned future will complete with `AccessCredentials` if the user 290 | /// has given the application access to it's data. Otherwise the future will 291 | /// complete with a `UserConsentException`. 292 | /// 293 | /// In case another error occurs the returned future will complete with an 294 | /// `Exception`. 295 | /// 296 | /// [userPrompt] will be used for directing the user/user-agent to a URI. See 297 | /// [PromptUserForConsentManual] for more information. 298 | /// 299 | /// [client] will be used for obtaining `AccessCredentials` from an 300 | /// authorization code. 301 | /// 302 | /// The [ClientId] can be obtained in the Google Cloud Console. 303 | Future obtainAccessCredentialsViaUserConsentManual( 304 | ClientId clientId, 305 | List scopes, 306 | Client client, 307 | PromptUserForConsentManual userPrompt) { 308 | return new AuthorizationCodeGrantManualFlow( 309 | clientId, scopes, client, userPrompt) 310 | .run(); 311 | } 312 | 313 | /// Obtain oauth2 [AccessCredentials] using service account credentials. 314 | /// 315 | /// In case the service account has no access to the requested scopes or another 316 | /// error occurs the returned future will complete with an `Exception`. 317 | /// 318 | /// [baseClient] will be used for obtaining `AccessCredentials`. 319 | /// 320 | /// The [ServiceAccountCredentials] can be obtained in the Google Cloud Console. 321 | Future obtainAccessCredentialsViaServiceAccount( 322 | ServiceAccountCredentials clientCredentials, 323 | List scopes, 324 | Client baseClient) { 325 | return new JwtFlow(clientCredentials.email, clientCredentials.privateRSAKey, 326 | clientCredentials.impersonatedUser, scopes, baseClient) 327 | .run(); 328 | } 329 | 330 | /// Obtain oauth2 [AccessCredentials] using the metadata API on ComputeEngine. 331 | /// 332 | /// In case the VM was not configured with access to the requested scopes or an 333 | /// error occurs the returned future will complete with an `Exception`. 334 | /// 335 | /// [baseClient] will be used for obtaining `AccessCredentials`. 336 | /// 337 | /// No credentials are needed. But this function is only intended to work on a 338 | /// Google Compute Engine VM with configured access to Google APIs. 339 | Future obtainAccessCredentialsViaMetadataServer( 340 | Client baseClient) { 341 | return new MetadataServerAuthorizationFlow(baseClient).run(); 342 | } 343 | 344 | /// Obtain oauth2 [AccessCredentials] by exchanging an authorization code. 345 | /// 346 | /// Running a hybrid oauth2 flow as described in the 347 | /// `googleapis_auth.auth_browser` library results in a `HybridFlowResult` which 348 | /// contains short-lived [AccessCredentials] for the client and an authorization 349 | /// code. This authorization code needs to be transferred to the server, which 350 | /// can exchange it against long-lived [AccessCredentials]. 351 | /// 352 | /// If the authorization code was obtained using the mentioned hybrid flow, the 353 | /// [redirectUrl] must be `"postmessage"` (default). 354 | /// 355 | /// If you obtained the authorization code using a different mechanism, the 356 | /// [redirectUrl] must be the same that was used to obtain the code. 357 | /// 358 | /// NOTE: Only the server application will know the `client secret` - which is 359 | /// necessary to exchange an authorization code against access tokens. 360 | /// 361 | /// NOTE: It is important to transmit the authorization code in a secure manner 362 | /// to the server. You should use "anti-request forgery state tokens" to guard 363 | /// against "cross site request forgery" attacks. 364 | /// An example on how to do this can be found here: 365 | /// https://developers.google.com/+/web/signin/server-side-flow 366 | Future obtainAccessCredentialsViaCodeExchange( 367 | Client baseClient, ClientId clientId, String code, 368 | {String redirectUrl: 'postmessage'}) { 369 | return obtainAccessCredentialsUsingCode( 370 | clientId, code, redirectUrl, baseClient); 371 | } 372 | 373 | /// Will close the underlying `http.Client`. 374 | class _ServiceAccountClient extends AutoRefreshDelegatingClient { 375 | final JwtFlow flow; 376 | AccessCredentials credentials; 377 | late Client authClient; 378 | 379 | _ServiceAccountClient(Client client, this.credentials, this.flow) 380 | : super(client) { 381 | authClient = authenticatedClient(baseClient, credentials); 382 | } 383 | 384 | Future send(BaseRequest request) async { 385 | if (!credentials.accessToken.hasExpired) { 386 | return authClient.send(request); 387 | } else { 388 | var newCredentials = await flow.run(); 389 | notifyAboutNewCredentials(newCredentials); 390 | credentials = newCredentials; 391 | authClient = authenticatedClient(baseClient, credentials); 392 | return authClient.send(request); 393 | } 394 | } 395 | } 396 | 397 | /// Will close the underlying `http.Client`. 398 | class _MetadataServerClient extends AutoRefreshDelegatingClient { 399 | final MetadataServerAuthorizationFlow flow; 400 | AccessCredentials credentials; 401 | Client authClient; 402 | 403 | _MetadataServerClient(Client client, this.credentials, this.flow) 404 | : authClient = authenticatedClient(client, credentials), 405 | super(client); 406 | 407 | Future send(BaseRequest request) async { 408 | if (!credentials.accessToken.hasExpired) { 409 | return authClient.send(request); 410 | } 411 | 412 | var newCredentials = await flow.run(); 413 | notifyAboutNewCredentials(newCredentials); 414 | credentials = newCredentials; 415 | authClient = authenticatedClient(baseClient, credentials); 416 | return authClient.send(request); 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /lib/src/adc_utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:convert'; 3 | import 'dart:async'; 4 | 5 | import 'package:http/http.dart'; 6 | 7 | import 'auth_http_utils.dart'; 8 | import '../auth_io.dart'; 9 | 10 | Future fromApplicationsCredentialsFile( 11 | File file, 12 | String fileSource, 13 | List scopes, 14 | Client baseClient, 15 | ) async { 16 | var credentials; 17 | try { 18 | credentials = json.decode(await file.readAsString()); 19 | } on IOException { 20 | throw Exception( 21 | 'Failed to read credentials file from $fileSource', 22 | ); 23 | } on FormatException { 24 | throw Exception( 25 | 'Failed to parse JSON from credentials file from $fileSource', 26 | ); 27 | } 28 | 29 | if (credentials is Map && credentials['type'] == 'authorized_user') { 30 | final clientId = ClientId( 31 | credentials['client_id'], 32 | credentials['client_secret'], 33 | ); 34 | return AutoRefreshingClient( 35 | baseClient, 36 | clientId, 37 | await refreshCredentials( 38 | clientId, 39 | AccessCredentials( 40 | // Hack: Create empty credentials that have expired. 41 | AccessToken('Bearer', '', DateTime(0).toUtc()), 42 | credentials['refresh_token'], 43 | scopes, 44 | ), 45 | baseClient, 46 | ), 47 | quotaProject: credentials["quota_project_id"], 48 | ); 49 | } 50 | return await clientViaServiceAccount( 51 | ServiceAccountCredentials.fromJson(credentials), 52 | scopes, 53 | baseClient: baseClient, 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/auth_http_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth; 6 | 7 | import 'dart:async'; 8 | 9 | import 'package:http/http.dart'; 10 | 11 | import '../auth.dart'; 12 | import 'http_client_base.dart'; 13 | 14 | /// Will close the underlying `http.Client` depending on a constructor argument. 15 | class AuthenticatedClient extends DelegatingClient implements AuthClient { 16 | final AccessCredentials credentials; 17 | final String? quotaProject; 18 | 19 | AuthenticatedClient(Client client, this.credentials, {this.quotaProject}) 20 | : super(client, closeUnderlyingClient: false); 21 | 22 | Future send(BaseRequest request) async { 23 | // Make new request object and perform the authenticated request. 24 | var modifiedRequest = 25 | new RequestImpl(request.method, request.url, request.finalize()); 26 | modifiedRequest.headers.addAll(request.headers); 27 | modifiedRequest.headers['Authorization'] = 28 | 'Bearer ${credentials.accessToken.data}'; 29 | if (quotaProject != null) { 30 | modifiedRequest.headers['X-Goog-User-Project'] = quotaProject!; 31 | } 32 | var response = await baseClient.send(modifiedRequest); 33 | var wwwAuthenticate = response.headers['www-authenticate']; 34 | if (wwwAuthenticate != null) { 35 | await response.stream.drain(); 36 | throw new AccessDeniedException('Access was denied ' 37 | '(www-authenticate header was: $wwwAuthenticate).'); 38 | } 39 | return response; 40 | } 41 | } 42 | 43 | /// Adds 'key' query parameter when making HTTP requests. 44 | /// 45 | /// If 'key' is already present on the URI, it will complete with an exception. 46 | /// This will prevent accidental overrides of a query parameter with the API 47 | /// key. 48 | class ApiKeyClient extends DelegatingClient { 49 | final String _encodedApiKey; 50 | 51 | ApiKeyClient(Client client, String apiKey) 52 | : _encodedApiKey = Uri.encodeQueryComponent(apiKey), 53 | super(client, closeUnderlyingClient: true); 54 | 55 | Future send(BaseRequest request) { 56 | var url = request.url; 57 | if (url.queryParameters.containsKey('key')) { 58 | return new Future.error(new Exception( 59 | 'Tried to make a HTTP request which has already a "key" query ' 60 | 'parameter. Adding the API key would override that existing value.')); 61 | } 62 | 63 | if (url.query == '') { 64 | url = url.replace(query: 'key=$_encodedApiKey'); 65 | } else { 66 | url = url.replace(query: '${url.query}&key=$_encodedApiKey'); 67 | } 68 | 69 | var modifiedRequest = 70 | new RequestImpl(request.method, url, request.finalize()); 71 | modifiedRequest.headers.addAll(request.headers); 72 | return baseClient.send(modifiedRequest); 73 | } 74 | } 75 | 76 | /// Will close the underlying `http.Client` depending on a constructor argument. 77 | class AutoRefreshingClient extends AutoRefreshDelegatingClient { 78 | final ClientId clientId; 79 | final String? quotaProject; 80 | AccessCredentials credentials; 81 | late Client authClient; 82 | 83 | AutoRefreshingClient(Client client, this.clientId, this.credentials, 84 | {bool closeUnderlyingClient: false, this.quotaProject}) 85 | : super(client, closeUnderlyingClient: closeUnderlyingClient) { 86 | assert(credentials.accessToken.type == 'Bearer'); 87 | assert(credentials.refreshToken != null); 88 | authClient = AuthenticatedClient( 89 | baseClient, 90 | credentials, 91 | quotaProject: quotaProject, 92 | ); 93 | } 94 | 95 | Future send(BaseRequest request) async { 96 | if (!credentials.accessToken.hasExpired) { 97 | // TODO: Can this return a "access token expired" message? 98 | // If so, we should handle it. 99 | return authClient.send(request); 100 | } else { 101 | var cred = await refreshCredentials(clientId, credentials, baseClient); 102 | notifyAboutNewCredentials(cred); 103 | credentials = cred; 104 | authClient = AuthenticatedClient( 105 | baseClient, 106 | cred, 107 | quotaProject: quotaProject, 108 | ); 109 | return authClient.send(request); 110 | } 111 | } 112 | } 113 | 114 | abstract class AutoRefreshDelegatingClient extends DelegatingClient 115 | implements AutoRefreshingAuthClient { 116 | final StreamController _credentialStreamController = 117 | new StreamController.broadcast(sync: true); 118 | 119 | AutoRefreshDelegatingClient(Client client, {bool closeUnderlyingClient: true}) 120 | : super(client, closeUnderlyingClient: closeUnderlyingClient); 121 | 122 | Stream get credentialUpdates => 123 | _credentialStreamController.stream; 124 | 125 | void notifyAboutNewCredentials(AccessCredentials credentials) { 126 | _credentialStreamController.add(credentials); 127 | } 128 | 129 | void close() { 130 | _credentialStreamController.close(); 131 | super.close(); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /lib/src/crypto/asn1.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 appengine_auth.asn; 6 | 7 | import 'dart:typed_data'; 8 | 9 | import 'rsa.dart'; 10 | 11 | class ASN1Parser { 12 | static const INTEGER_TAG = 0x02; 13 | static const OCTET_STRING_TAG = 0x04; 14 | static const NULL_TAG = 0x05; 15 | static const OBJECT_ID_TAG = 0x06; 16 | static const SEQUENCE_TAG = 0x30; 17 | 18 | static ASN1Object parse(Uint8List bytes) { 19 | invalidFormat(String msg) { 20 | throw new ArgumentError("Invalid DER encoding: $msg"); 21 | } 22 | 23 | var data = new ByteData.view(bytes.buffer); 24 | int offset = 0; 25 | int end = bytes.length; 26 | 27 | checkNBytesAvailable(int n) { 28 | if ((offset + n) > end) { 29 | invalidFormat('Tried to read more bytes than available.'); 30 | } 31 | } 32 | 33 | List readBytes(int n) { 34 | checkNBytesAvailable(n); 35 | 36 | var integerBytes = bytes.sublist(offset, offset + n); 37 | offset += n; 38 | return integerBytes; 39 | } 40 | 41 | int readEncodedLength() { 42 | checkNBytesAvailable(1); 43 | 44 | var lengthByte = data.getUint8(offset++); 45 | 46 | // Short length encoding form: This byte is the length itself. 47 | if (lengthByte < 0x80) { 48 | return lengthByte; 49 | } 50 | 51 | // Long length encoding form: 52 | // This byte has in bits 0..6 the number of bytes following which encode 53 | // the length. 54 | int countLengthBytes = lengthByte & 0x7f; 55 | checkNBytesAvailable(countLengthBytes); 56 | 57 | int length = 0; 58 | while (countLengthBytes > 0) { 59 | length = (length << 8) | data.getUint8(offset++); 60 | countLengthBytes--; 61 | } 62 | return length; 63 | } 64 | 65 | void readNullBytes() { 66 | checkNBytesAvailable(1); 67 | var nullByte = data.getUint8(offset++); 68 | if (nullByte != 0x00) { 69 | invalidFormat('Null byte expect, but was: $nullByte.'); 70 | } 71 | } 72 | 73 | ASN1Object decodeObject() { 74 | checkNBytesAvailable(1); 75 | var tag = bytes[offset++]; 76 | switch (tag) { 77 | case INTEGER_TAG: 78 | int size = readEncodedLength(); 79 | return new ASN1Integer(RSAAlgorithm.bytes2BigInt(readBytes(size))); 80 | case OCTET_STRING_TAG: 81 | var size = readEncodedLength(); 82 | return new ASN1OctetString(readBytes(size)); 83 | case NULL_TAG: 84 | readNullBytes(); 85 | return new ASN1Null(); 86 | case OBJECT_ID_TAG: 87 | var size = readEncodedLength(); 88 | return new ASN1ObjectIdentifier(readBytes(size)); 89 | case SEQUENCE_TAG: 90 | var lengthInBytes = readEncodedLength(); 91 | if ((offset + lengthInBytes) > end) { 92 | invalidFormat('Tried to read more bytes than available.'); 93 | } 94 | int endOfSequence = offset + lengthInBytes; 95 | 96 | var objects = []; 97 | while (offset < endOfSequence) { 98 | objects.add(decodeObject()); 99 | } 100 | return new ASN1Sequence(objects); 101 | default: 102 | invalidFormat( 103 | 'Unexpected tag $tag at offset ${offset - 1} (end: $end).'); 104 | } 105 | } 106 | 107 | var obj = decodeObject(); 108 | if (offset != bytes.length) { 109 | throw new ArgumentError('More bytes than expected in ASN1 encoding.'); 110 | } 111 | return obj; 112 | } 113 | } 114 | 115 | abstract class ASN1Object {} 116 | 117 | class ASN1Sequence extends ASN1Object { 118 | final List objects; 119 | ASN1Sequence(this.objects); 120 | } 121 | 122 | class ASN1Integer extends ASN1Object { 123 | final BigInt integer; 124 | ASN1Integer(this.integer); 125 | } 126 | 127 | class ASN1OctetString extends ASN1Object { 128 | final List bytes; 129 | ASN1OctetString(this.bytes); 130 | } 131 | 132 | class ASN1ObjectIdentifier extends ASN1Object { 133 | final List bytes; 134 | ASN1ObjectIdentifier(this.bytes); 135 | } 136 | 137 | class ASN1Null extends ASN1Object {} 138 | -------------------------------------------------------------------------------- /lib/src/crypto/pem.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.pem; 6 | 7 | import 'dart:convert'; 8 | import 'dart:typed_data'; 9 | 10 | import 'asn1.dart'; 11 | import 'rsa.dart'; 12 | 13 | /// Decode a [RSAPrivateKey] from the string content of a PEM file. 14 | /// 15 | /// A PEM file can be extracted from a .p12 cryptostore with 16 | /// $ openssl pkcs12 -nocerts -nodes -passin pass:notasecret \ 17 | /// -in *-privatekey.p12 -out *-privatekey.pem 18 | RSAPrivateKey keyFromString(String pemFileString) { 19 | if (pemFileString == null) { 20 | throw new ArgumentError('Argument must not be null.'); 21 | } 22 | var bytes = _getBytesFromPEMString(pemFileString); 23 | return _extractRSAKeyFromDERBytes(bytes); 24 | } 25 | 26 | /// Helper function for decoding the base64 in [pemString]. 27 | Uint8List _getBytesFromPEMString(String pemString) { 28 | var lines = LineSplitter.split(pemString) 29 | .map((line) => line.trim()) 30 | .where((line) => line.isNotEmpty) 31 | .toList(); 32 | 33 | if (lines.length < 2 || 34 | !lines.first.startsWith('-----BEGIN') || 35 | !lines.last.startsWith('-----END')) { 36 | throw new ArgumentError('The given string does not have the correct ' 37 | 'begin/end markers expected in a PEM file.'); 38 | } 39 | var base64 = lines.sublist(1, lines.length - 1).join(''); 40 | return new Uint8List.fromList(base64Decode(base64)); 41 | } 42 | 43 | /// Helper to decode the ASN.1/DER bytes in [bytes] into an [RSAPrivateKey]. 44 | RSAPrivateKey _extractRSAKeyFromDERBytes(Uint8List bytes) { 45 | // We recognize two formats: 46 | // Real format: 47 | // 48 | // PrivateKey := seq[int/version=0, int/n, int/e, int/d, int/p, 49 | // int/q, int/dmp1, int/dmq1, int/coeff] 50 | // 51 | // Or the above `PrivateKey` embeddded inside another ASN object: 52 | // Encapsulated := seq[int/version=0, 53 | // seq[obj-id/rsa-id, null-obj], 54 | // octet-string/PrivateKey] 55 | // 56 | 57 | RSAPrivateKey privateKeyFromSequence(ASN1Sequence asnSequence) { 58 | var objects = asnSequence.objects; 59 | 60 | var asnIntegers = objects.take(9).map((o) => o as ASN1Integer).toList(); 61 | 62 | var version = asnIntegers.first; 63 | if (version.integer != BigInt.zero) { 64 | throw new ArgumentError('Expected version 0, got: ${version.integer}.'); 65 | } 66 | 67 | var key = new RSAPrivateKey( 68 | asnIntegers[1].integer, 69 | asnIntegers[2].integer, 70 | asnIntegers[3].integer, 71 | asnIntegers[4].integer, 72 | asnIntegers[5].integer, 73 | asnIntegers[6].integer, 74 | asnIntegers[7].integer, 75 | asnIntegers[8].integer); 76 | 77 | var bitLength = key.bitLength; 78 | if (bitLength != 1024 && bitLength != 2048 && bitLength != 4096) { 79 | throw new ArgumentError('The RSA modulus has a bit length of $bitLength. ' 80 | 'Only 1024, 2048 and 4096 are supported.'); 81 | } 82 | return key; 83 | } 84 | 85 | try { 86 | var asn = ASN1Parser.parse(bytes); 87 | if (asn is ASN1Sequence) { 88 | var objects = asn.objects; 89 | if (objects.length == 3 && objects[2] is ASN1OctetString) { 90 | var string = objects[2] as ASN1OctetString; 91 | // Seems like the embedded form. 92 | // TODO: Validate that rsa identifier matches! 93 | return privateKeyFromSequence( 94 | ASN1Parser.parse(string.bytes as Uint8List) as ASN1Sequence); 95 | } 96 | } 97 | return privateKeyFromSequence(asn as ASN1Sequence); 98 | } catch (error) { 99 | throw new ArgumentError( 100 | 'Error while extracting private key from DER bytes: $error'); 101 | } 102 | } 103 | 104 | /* 105 | * Example of generating a public/private RSA keypair with 2048 bits and dumping 106 | * the structure of the resulting private key. 107 | 108 | $ openssl genrsa -out key.pem 2048 109 | Generating RSA private key, 2048 bit long modulus 110 | ..................................................+++ 111 | ..................................................+++ 112 | e is 65537 (0x10001) 113 | 114 | $ cat key.pem 115 | -----BEGIN RSA PRIVATE KEY----- 116 | MIIEowIBAAKCAQEAuDOwXO14ltE1j2O0iDSuqtbw/1kMKjeiki3oehk2zNoUte42 117 | /s2rX15nYCkKtYG/r8WYvKzb31P4Uow1S4fFydKNWxgX4VtEjHgeqfPxeCL9wiJc 118 | 9KkEt4fyhj1Jo7193gCLtovLAFwPzAMbFLiXWkfqalJ5Z77fOE4Mo7u4pEgxNPgL 119 | VFGe0cEOAsHsKlsze+m1pmPHwWNVTcoKe5o0hOzy6hCPgVc6me6Y7aO8Fb4OVg0l 120 | XQdQpWn2ikVBpzBcZ6InnYyJ/CJNa3WL1LJ65mmYnfHtKGoMqhLK48OReguwRwwF 121 | e9/2+8UcdZcN5rsvt7yg3ZrKNH8rx+wZ36sRewIDAQABAoIBAQCn1HCcOsHkqDlk 122 | rDOQ5m8+uRhbj4bF8GrvRWTL2q1TeF/mY2U4Q6wg+KK3uq1HMzCzthWz0suCb7+R 123 | dq4YY1ySxoSEuy8G5WFPmyJVNy6Lh1Yty6FmSZlCn1sZdD3kMoK8A0NIz5Xmffrm 124 | pu3Fs2ozl9K9jOeQ3xgC9RoPFLrm8lHJ45Vn+SnTxZnsXT6pwpg3TnFIx5ZinU8k 125 | l0Um1n80qD2QQDakQ5jyr2odAELLBDlyCkxAglBXAVt4nk9Kl6nxb4snd9dnrL70 126 | WjLynWQsDczaV9TZIl2hYkMud+9OLVlUUtB+0c5b0p2t2P0sLltDaq3H6pT6yu2G 127 | 8E86J9IBAoGBAPJaTNV5ysVOFn+YwWwRztzrvNArUJkVq8abN0gGp3gUvDEZnvzK 128 | weF7+lfZzcwVRmQkL3mWLzzZvCx77RfulAzLi5iFuRBPhhhxAPDiDuyL9B7O81G/ 129 | M/W5DPctGOyD/9cnLuh72oij0unc5MLSfzJf8wblpcjJnPBDqIVh6Qt9AoGBAMKT 130 | Gacf4iSj1xW+0wrnbZlDuyCl6Msptj8ePcvLQrFqQmBwsXmWgVR+gFc/1G3lRft0 131 | QC6chsmafQHIIPpaDjq3sQ01/tUu7LXL+g/Hw9XtUHbkg3sZIQBtC26rKdStfHNS 132 | KTvuCgn/dAJNjiohfhWMt9R4Q6E5FV6PqQHJzPJXAoGAC41qZDKuC8GxKNvrPG+M 133 | 4NML6RBngySZT5pOhExs5zh10BFclshDfbAfOtjTCotpE5T1/mG+VrQ6WBSANMfW 134 | ntWFDfwx2ikwRzH7zX+5HmV9eYp75sWqgGgVyiKIMZ4JMARaJBLjU+gbQbKZ5P+L 135 | uKcCOq3vvSZ/KKTQ/6qvJTECgYBiWgbCgoxF5wdmd4Gn5llw+lqRYyur3hbACuJD 136 | rCe3FDYfF3euNRSEiDkJYTtYnWbldtqmdPpw14VOrEF3KqQ8q/Nz8RIx4jlGn6dz 137 | 6I8mCIH+xv1q8MXMuFHqC9zmIxdgF2y+XVF3wkd6jodI5oscC3g0juHokbkqhkVw 138 | oPfWmwKBgBfR6jv0gWWeWTfkNwj+cMLHQV1uvz6JyLH5K4iISEDFxYkd37jrHB8A 139 | 9hz9UDfmCbSs2j8CXDg7zCayM6tfu4Vtx+8S5g3oN6sa1JXFY1Os7SoXhTfX9M+7 140 | QpYYDJZwkgZrVQoKMIdCs9xfyVhZERq945NYLekwE1t2W+tOVBgR 141 | -----END RSA PRIVATE KEY----- 142 | 143 | $ openssl enc -d -base64 -in key.pem -out key.bin 144 | 145 | $ dumpasn1 key.bin 146 | 0 1187: SEQUENCE { 147 | 4 1: INTEGER 0 148 | 7 257: INTEGER 149 | : 00 B8 33 B0 5C ED 78 96 D1 35 8F 63 B4 88 34 AE 150 | : AA D6 F0 FF 59 0C 2A 37 A2 92 2D E8 7A 19 36 CC 151 | : DA 14 B5 EE 36 FE CD AB 5F 5E 67 60 29 0A B5 81 152 | : BF AF C5 98 BC AC DB DF 53 F8 52 8C 35 4B 87 C5 153 | : C9 D2 8D 5B 18 17 E1 5B 44 8C 78 1E A9 F3 F1 78 154 | : 22 FD C2 22 5C F4 A9 04 B7 87 F2 86 3D 49 A3 BD 155 | : 7D DE 00 8B B6 8B CB 00 5C 0F CC 03 1B 14 B8 97 156 | : 5A 47 EA 6A 52 79 67 BE DF 38 4E 0C A3 BB B8 A4 157 | : [ Another 129 bytes skipped ] 158 | 268 3: INTEGER 65537 159 | 273 257: INTEGER 160 | : 00 A7 D4 70 9C 3A C1 E4 A8 39 64 AC 33 90 E6 6F 161 | : 3E B9 18 5B 8F 86 C5 F0 6A EF 45 64 CB DA AD 53 162 | : 78 5F E6 63 65 38 43 AC 20 F8 A2 B7 BA AD 47 33 163 | : 30 B3 B6 15 B3 D2 CB 82 6F BF 91 76 AE 18 63 5C 164 | : 92 C6 84 84 BB 2F 06 E5 61 4F 9B 22 55 37 2E 8B 165 | : 87 56 2D CB A1 66 49 99 42 9F 5B 19 74 3D E4 32 166 | : 82 BC 03 43 48 CF 95 E6 7D FA E6 A6 ED C5 B3 6A 167 | : 33 97 D2 BD 8C E7 90 DF 18 02 F5 1A 0F 14 BA E6 168 | : [ Another 129 bytes skipped ] 169 | 534 129: INTEGER 170 | : 00 F2 5A 4C D5 79 CA C5 4E 16 7F 98 C1 6C 11 CE 171 | : DC EB BC D0 2B 50 99 15 AB C6 9B 37 48 06 A7 78 172 | : 14 BC 31 19 9E FC CA C1 E1 7B FA 57 D9 CD CC 15 173 | : 46 64 24 2F 79 96 2F 3C D9 BC 2C 7B ED 17 EE 94 174 | : 0C CB 8B 98 85 B9 10 4F 86 18 71 00 F0 E2 0E EC 175 | : 8B F4 1E CE F3 51 BF 33 F5 B9 0C F7 2D 18 EC 83 176 | : FF D7 27 2E E8 7B DA 88 A3 D2 E9 DC E4 C2 D2 7F 177 | : 32 5F F3 06 E5 A5 C8 C9 9C F0 43 A8 85 61 E9 0B 178 | : [ Another 1 bytes skipped ] 179 | 666 129: INTEGER 180 | : 00 C2 93 19 A7 1F E2 24 A3 D7 15 BE D3 0A E7 6D 181 | : 99 43 BB 20 A5 E8 CB 29 B6 3F 1E 3D CB CB 42 B1 182 | : 6A 42 60 70 B1 79 96 81 54 7E 80 57 3F D4 6D E5 183 | : 45 FB 74 40 2E 9C 86 C9 9A 7D 01 C8 20 FA 5A 0E 184 | : 3A B7 B1 0D 35 FE D5 2E EC B5 CB FA 0F C7 C3 D5 185 | : ED 50 76 E4 83 7B 19 21 00 6D 0B 6E AB 29 D4 AD 186 | : 7C 73 52 29 3B EE 0A 09 FF 74 02 4D 8E 2A 21 7E 187 | : 15 8C B7 D4 78 43 A1 39 15 5E 8F A9 01 C9 CC F2 188 | : [ Another 1 bytes skipped ] 189 | 798 128: INTEGER 190 | : 0B 8D 6A 64 32 AE 0B C1 B1 28 DB EB 3C 6F 8C E0 191 | : D3 0B E9 10 67 83 24 99 4F 9A 4E 84 4C 6C E7 38 192 | : 75 D0 11 5C 96 C8 43 7D B0 1F 3A D8 D3 0A 8B 69 193 | : 13 94 F5 FE 61 BE 56 B4 3A 58 14 80 34 C7 D6 9E 194 | : D5 85 0D FC 31 DA 29 30 47 31 FB CD 7F B9 1E 65 195 | : 7D 79 8A 7B E6 C5 AA 80 68 15 CA 22 88 31 9E 09 196 | : 30 04 5A 24 12 E3 53 E8 1B 41 B2 99 E4 FF 8B B8 197 | : A7 02 3A AD EF BD 26 7F 28 A4 D0 FF AA AF 25 31 198 | 929 128: INTEGER 199 | : 62 5A 06 C2 82 8C 45 E7 07 66 77 81 A7 E6 59 70 200 | : FA 5A 91 63 2B AB DE 16 C0 0A E2 43 AC 27 B7 14 201 | : 36 1F 17 77 AE 35 14 84 88 39 09 61 3B 58 9D 66 202 | : E5 76 DA A6 74 FA 70 D7 85 4E AC 41 77 2A A4 3C 203 | : AB F3 73 F1 12 31 E2 39 46 9F A7 73 E8 8F 26 08 204 | : 81 FE C6 FD 6A F0 C5 CC B8 51 EA 0B DC E6 23 17 205 | : 60 17 6C BE 5D 51 77 C2 47 7A 8E 87 48 E6 8B 1C 206 | : 0B 78 34 8E E1 E8 91 B9 2A 86 45 70 A0 F7 D6 9B 207 | 1060 128: INTEGER 208 | : 17 D1 EA 3B F4 81 65 9E 59 37 E4 37 08 FE 70 C2 209 | : C7 41 5D 6E BF 3E 89 C8 B1 F9 2B 88 88 48 40 C5 210 | : C5 89 1D DF B8 EB 1C 1F 00 F6 1C FD 50 37 E6 09 211 | : B4 AC DA 3F 02 5C 38 3B CC 26 B2 33 AB 5F BB 85 212 | : 6D C7 EF 12 E6 0D E8 37 AB 1A D4 95 C5 63 53 AC 213 | : ED 2A 17 85 37 D7 F4 CF BB 42 96 18 0C 96 70 92 214 | : 06 6B 55 0A 0A 30 87 42 B3 DC 5F C9 58 59 11 1A 215 | : BD E3 93 58 2D E9 30 13 5B 76 5B EB 4E 54 18 11 216 | : } 217 | */ 218 | -------------------------------------------------------------------------------- /lib/src/crypto/rsa.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | // A small part is based on a JavaScript implementation of RSA by Tom Wu 6 | // but re-written in dart. 7 | 8 | library googleapis_auth.rsa; 9 | 10 | import 'dart:typed_data'; 11 | 12 | /// Represents integers obtained while creating a Public/Private key pair. 13 | class RSAPrivateKey { 14 | /// First prime number. 15 | final BigInt p; 16 | 17 | /// Second prime number. 18 | final BigInt q; 19 | 20 | /// Modulus for public and private keys. Satisfies `n=p*q`. 21 | final BigInt n; 22 | 23 | /// Public key exponent. Satisfies `d*e=1 mod phi(n)`. 24 | final BigInt e; 25 | 26 | /// Private key exponent. Satisfies `d*e=1 mod phi(n)`. 27 | final BigInt d; 28 | 29 | /// Different form of [p]. Satisfies `dmp1=d mod (p-1)`. 30 | final BigInt dmp1; 31 | 32 | /// Different form of [p]. Satisfies `dmq1=d mod (q-1)`. 33 | final BigInt dmq1; 34 | 35 | /// A coefficient which satisfies `coeff=q^-1 mod p`. 36 | final BigInt coeff; 37 | 38 | /// The number of bits used for the modulus. Usually 1024, 2048 or 4096 bits. 39 | int get bitLength => n.bitLength; 40 | 41 | RSAPrivateKey( 42 | this.n, this.e, this.d, this.p, this.q, this.dmp1, this.dmq1, this.coeff); 43 | } 44 | 45 | /// Provides a [encrypt] method for encrypting messages with a [RSAPrivateKey]. 46 | abstract class RSAAlgorithm { 47 | /// Performs the encryption of [bytes] with the private [key]. 48 | /// Others who have access to the public key will be able to decrypt this 49 | /// the result. 50 | /// 51 | /// The [intendedLength] argument specifies the number of bytes in which the 52 | /// result should be encoded. Zero bytes will be used for padding. 53 | static List encrypt( 54 | RSAPrivateKey key, List bytes, int intendedLength) { 55 | var message = bytes2BigInt(bytes); 56 | var encryptedMessage = _encryptInteger(key, message); 57 | return integer2Bytes(encryptedMessage, intendedLength); 58 | } 59 | 60 | static BigInt _encryptInteger(RSAPrivateKey key, BigInt x) { 61 | // The following is equivalent to `_modPow(x, key.d, key.n) but is much 62 | // more efficient. It exploits the fact that we have dmp1/dmq1. 63 | var xp = _modPow(x % key.p, key.dmp1, key.p); 64 | var xq = _modPow(x % key.q, key.dmq1, key.q); 65 | while (xp < xq) { 66 | xp += key.p; 67 | } 68 | return ((((xp - xq) * key.coeff) % key.p) * key.q) + xq; 69 | } 70 | 71 | // TODO(kevmoo): see if this can be done more efficiently with BigInt 72 | static BigInt _modPow(BigInt b, BigInt e, BigInt m) { 73 | if (e < BigInt.one) { 74 | return BigInt.one; 75 | } 76 | if (b < BigInt.zero || b > m) { 77 | b = b % m; 78 | } 79 | var r = BigInt.one; 80 | while (e > BigInt.zero) { 81 | if ((e & BigInt.one) > BigInt.zero) { 82 | r = (r * b) % m; 83 | } 84 | e >>= 1; 85 | b = (b * b) % m; 86 | } 87 | return r; 88 | } 89 | 90 | static BigInt bytes2BigInt(List bytes) { 91 | var number = BigInt.zero; 92 | for (var i = 0; i < bytes.length; i++) { 93 | number = (number << 8) | new BigInt.from(bytes[i]); 94 | } 95 | return number; 96 | } 97 | 98 | static List integer2Bytes(BigInt integer, int intendedLength) { 99 | if (integer < BigInt.one) { 100 | throw new ArgumentError('Only positive integers are supported.'); 101 | } 102 | var bytes = new Uint8List(intendedLength); 103 | for (int i = bytes.length - 1; i >= 0; i--) { 104 | bytes[i] = (integer & _bigIntFF).toInt(); 105 | integer >>= 8; 106 | } 107 | return bytes; 108 | } 109 | } 110 | 111 | final _bigIntFF = new BigInt.from(0xff); 112 | -------------------------------------------------------------------------------- /lib/src/crypto/rsa_sign.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.rsa_sign; 6 | 7 | import 'dart:typed_data'; 8 | 9 | import 'package:crypto/crypto.dart'; 10 | 11 | import 'asn1.dart'; 12 | import 'rsa.dart'; 13 | 14 | /// Used for signing messages with a private RSA key. 15 | /// 16 | /// The implemented algorithm can be seen in 17 | /// RFC 3447, Section 9.2 EMSA-PKCS1-v1_5. 18 | class RS256Signer { 19 | // NIST sha-256 OID (2 16 840 1 101 3 4 2 1) 20 | // See a reference for the encoding here: 21 | // http://msdn.microsoft.com/en-us/library/bb540809%28v=vs.85%29.aspx 22 | static const _RSA_SHA256_ALGORITHM_IDENTIFIER = const [ 23 | 0x06, 24 | 0x09, 25 | 0x60, 26 | 0x86, 27 | 0x48, 28 | 0x01, 29 | 0x65, 30 | 0x03, 31 | 0x04, 32 | 0x02, 33 | 0x01 34 | ]; 35 | 36 | final RSAPrivateKey _rsaKey; 37 | 38 | RS256Signer(this._rsaKey); 39 | 40 | List sign(List bytes) { 41 | var digest = _digestInfo(sha256.convert(bytes).bytes); 42 | var modulusLen = (_rsaKey.bitLength + 7) ~/ 8; 43 | 44 | var block = new Uint8List(modulusLen); 45 | var padLength = block.length - digest.length - 3; 46 | block[0] = 0x00; 47 | block[1] = 0x01; 48 | block.fillRange(2, 2 + padLength, 0xFF); 49 | block[2 + padLength] = 0x00; 50 | block.setRange(2 + padLength + 1, block.length, digest); 51 | return RSAAlgorithm.encrypt(_rsaKey, block, modulusLen); 52 | } 53 | 54 | static Uint8List _digestInfo(List hash) { 55 | // DigestInfo :== SEQUENCE { 56 | // digestAlgorithm AlgorithmIdentifier, 57 | // digest OCTET STRING 58 | // } 59 | var offset = 0; 60 | var digestInfo = new Uint8List( 61 | 2 + 2 + _RSA_SHA256_ALGORITHM_IDENTIFIER.length + 2 + 2 + hash.length); 62 | { 63 | // DigestInfo 64 | digestInfo[offset++] = ASN1Parser.SEQUENCE_TAG; 65 | digestInfo[offset++] = digestInfo.length - 2; 66 | { 67 | // AlgorithmIdentifier. 68 | digestInfo[offset++] = ASN1Parser.SEQUENCE_TAG; 69 | digestInfo[offset++] = _RSA_SHA256_ALGORITHM_IDENTIFIER.length + 2; 70 | digestInfo.setAll(offset, _RSA_SHA256_ALGORITHM_IDENTIFIER); 71 | offset += _RSA_SHA256_ALGORITHM_IDENTIFIER.length; 72 | digestInfo[offset++] = ASN1Parser.NULL_TAG; 73 | digestInfo[offset++] = 0; 74 | } 75 | digestInfo[offset++] = ASN1Parser.OCTET_STRING_TAG; 76 | digestInfo[offset++] = hash.length; 77 | digestInfo.setAll(offset, hash); 78 | } 79 | return digestInfo; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/src/http_client_base.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.http_client_base; 6 | 7 | import 'dart:async'; 8 | 9 | import 'package:http/http.dart'; 10 | 11 | /// Base class for delegating HTTP clients. 12 | /// 13 | /// Depending on [closeUnderlyingClient] it will close the client it is 14 | /// delegating to or not. 15 | abstract class DelegatingClient extends BaseClient { 16 | final Client baseClient; 17 | final bool closeUnderlyingClient; 18 | bool _isClosed = false; 19 | 20 | DelegatingClient(this.baseClient, {this.closeUnderlyingClient: true}); 21 | 22 | void close() { 23 | if (_isClosed) { 24 | throw new StateError('Cannot close a HTTP client more than once.'); 25 | } 26 | _isClosed = true; 27 | super.close(); 28 | 29 | if (closeUnderlyingClient) { 30 | baseClient.close(); 31 | } 32 | } 33 | } 34 | 35 | /// A reference counted HTTP client. 36 | /// 37 | /// It uses a base [Client] which will be closed once the reference count 38 | /// reaches zero. The initial reference count is one, since the caller has a 39 | /// reference to the constructed instance. 40 | class RefCountedClient extends DelegatingClient { 41 | int _refCount; 42 | 43 | RefCountedClient(Client baseClient, {int initialRefCount: 1}) 44 | : _refCount = initialRefCount, 45 | super(baseClient, closeUnderlyingClient: true) { 46 | if (_refCount == null || _refCount <= 0) { 47 | throw new ArgumentError( 48 | 'A reference count of $initialRefCount is invalid.'); 49 | } 50 | } 51 | 52 | Future send(BaseRequest request) { 53 | _ensureClientIsOpen(); 54 | return baseClient.send(request); 55 | } 56 | 57 | /// Acquires a new reference which causes the reference count to be 58 | /// incremented by 1. 59 | void acquire() { 60 | _ensureClientIsOpen(); 61 | _refCount++; 62 | } 63 | 64 | /// Releases a new reference which causes the reference count to be 65 | /// decremented by 1. 66 | void release() { 67 | _ensureClientIsOpen(); 68 | _refCount--; 69 | 70 | if (_refCount == 0) { 71 | super.close(); 72 | } 73 | } 74 | 75 | /// Is equivalent to calling `release`. 76 | void close() { 77 | release(); 78 | } 79 | 80 | void _ensureClientIsOpen() { 81 | if (_refCount <= 0) { 82 | throw new StateError( 83 | 'This reference counted HTTP client has reached a count of zero and ' 84 | 'can no longer be used for making HTTP requests.'); 85 | } 86 | } 87 | } 88 | 89 | // NOTE: 90 | // Calling close on the returned client once will not close the underlying 91 | // [baseClient]. 92 | Client nonClosingClient(Client baseClient) => 93 | new RefCountedClient(baseClient, initialRefCount: 2); 94 | 95 | class RequestImpl extends BaseRequest { 96 | final Stream> _stream; 97 | 98 | RequestImpl(String method, Uri url, [Stream>? stream]) 99 | : _stream = stream ?? Stream.empty(), 100 | super(method, url); 101 | 102 | ByteStream finalize() { 103 | super.finalize(); 104 | return new ByteStream(_stream); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/src/oauth2_flows/auth_code.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.auth_code_flow; 6 | 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | import 'dart:io'; 10 | 11 | import 'package:http/http.dart' as http; 12 | 13 | import '../../auth.dart'; 14 | import '../http_client_base.dart'; 15 | import '../typedefs.dart'; 16 | import '../utils.dart'; 17 | 18 | // The OAuth2 Token endpoint can be used to make requests as 19 | // https://www.googleapis.com/oauth2/v2/tokeninfo?access_token= 20 | // 21 | // A successfull response from the server will give an HTTP response status 22 | // 200 and a body of the following type: 23 | // { 24 | // "issued_to": "XYZ.apps.googleusercontent.com", 25 | // "audience": "XYZ.apps.googleusercontent.com", 26 | // "scope": "https://www.googleapis.com/auth/bigquery", 27 | // "expires_in": 3547, 28 | // "access_type": "offline" 29 | // } 30 | // 31 | // Scopes are separated by spaces. 32 | Future> obtainScopesFromAccessToken( 33 | String accessToken, http.Client client) async { 34 | var url = Uri.parse('https://www.googleapis.com/oauth2/v2/tokeninfo' 35 | '?access_token=${Uri.encodeQueryComponent(accessToken)}'); 36 | 37 | var response = await client.post(url); 38 | if (response.statusCode == 200) { 39 | Map json = jsonDecode(response.body); 40 | var scope = json['scope']; 41 | if (scope is! String) { 42 | throw new Exception( 43 | 'The response did not include a `scope` value of type `String`.'); 44 | } 45 | return scope.split(' ').toList(); 46 | } else { 47 | throw new Exception('Unable to obtain list of scopes an access token ' 48 | 'is valid for. Server responded with ${response.statusCode}.'); 49 | } 50 | } 51 | 52 | Future obtainAccessCredentialsUsingCode( 53 | ClientId clientId, String code, String redirectUrl, http.Client client, 54 | [List? scopes]) async { 55 | var uri = Uri.parse('https://accounts.google.com/o/oauth2/token'); 56 | var formValues = [ 57 | 'grant_type=authorization_code', 58 | 'code=${Uri.encodeQueryComponent(code)}', 59 | 'redirect_uri=${Uri.encodeQueryComponent(redirectUrl)}', 60 | 'client_id=${Uri.encodeQueryComponent(clientId.identifier)}', 61 | 'client_secret=${Uri.encodeQueryComponent(clientId.secret!)}', 62 | ]; 63 | 64 | var body = new Stream>.fromIterable( 65 | >[ascii.encode(formValues.join('&'))]); 66 | var request = new RequestImpl('POST', uri, body); 67 | request.headers['content-type'] = CONTENT_TYPE_URLENCODED; 68 | 69 | var response = await client.send(request); 70 | Map jsonMap = (await utf8.decoder 71 | .bind(response.stream) 72 | .transform(json.decoder) 73 | .first) as Map; 74 | 75 | var idToken = jsonMap['id_token']; 76 | var tokenType = jsonMap['token_type']; 77 | var accessToken = jsonMap['access_token']; 78 | var seconds = jsonMap['expires_in']; 79 | var refreshToken = jsonMap['refresh_token']; 80 | var error = jsonMap['error']; 81 | 82 | if (response.statusCode != 200 && error != null) { 83 | throw new Exception('Failed to exchange authorization code. ' 84 | 'Response was ${response.statusCode}. Error message was $error.'); 85 | } 86 | 87 | if (response.statusCode != 200 || 88 | accessToken == null || 89 | seconds is! int || 90 | tokenType != 'Bearer') { 91 | throw new Exception('Failed to exchange authorization code. ' 92 | 'Invalid server response. ' 93 | 'Http status code was: ${response.statusCode}.'); 94 | } 95 | 96 | if (scopes != null) { 97 | return new AccessCredentials( 98 | new AccessToken('Bearer', accessToken, expiryDate(seconds)), 99 | refreshToken, 100 | scopes, 101 | idToken: idToken); 102 | } 103 | 104 | scopes = await obtainScopesFromAccessToken(accessToken, client); 105 | return new AccessCredentials( 106 | new AccessToken('Bearer', accessToken, expiryDate(seconds)), 107 | refreshToken, 108 | scopes, 109 | idToken: idToken); 110 | } 111 | 112 | /// Abstract class for obtaining access credentials via the authorization code 113 | /// grant flow 114 | /// 115 | /// See 116 | /// * [AuthorizationCodeGrantServerFlow] 117 | /// * [AuthorizationCodeGrantManualFlow] 118 | /// for further details. 119 | abstract class AuthorizationCodeGrantAbstractFlow { 120 | final ClientId clientId; 121 | final List scopes; 122 | final http.Client _client; 123 | 124 | AuthorizationCodeGrantAbstractFlow(this.clientId, this.scopes, this._client); 125 | 126 | Future run(); 127 | 128 | Future _obtainAccessCredentialsUsingCode( 129 | String code, String redirectUri) { 130 | return obtainAccessCredentialsUsingCode( 131 | clientId, code, redirectUri, _client, scopes); 132 | } 133 | 134 | String _authenticationUri(String redirectUri, {String? state}) { 135 | // TODO: Increase scopes with [include_granted_scopes]. 136 | var queryValues = [ 137 | 'response_type=code', 138 | 'client_id=${Uri.encodeQueryComponent(clientId.identifier)}', 139 | 'redirect_uri=${Uri.encodeQueryComponent(redirectUri)}', 140 | 'scope=${Uri.encodeQueryComponent(scopes.join(' '))}', 141 | ]; 142 | if (state != null) { 143 | queryValues.add('state=${Uri.encodeQueryComponent(state)}'); 144 | } 145 | return Uri.parse('https://accounts.google.com/o/oauth2/auth' 146 | '?${queryValues.join('&')}') 147 | .toString(); 148 | } 149 | } 150 | 151 | /// Runs an oauth2 authorization code grant flow using an HTTP server. 152 | /// 153 | /// This class is able to run an oauth2 authorization flow. It takes a user 154 | /// supplied function which will be called with an URI. The user is expected 155 | /// to navigate to that URI and to grant access to the client. 156 | /// 157 | /// Once the user has granted access to the client, Google will redirect the 158 | /// user agent to a URL pointing to a locally running HTTP server. Which in turn 159 | /// will be able to extract the authorization code from the URL and use it to 160 | /// obtain access credentials. 161 | class AuthorizationCodeGrantServerFlow 162 | extends AuthorizationCodeGrantAbstractFlow { 163 | final PromptUserForConsent userPrompt; 164 | 165 | AuthorizationCodeGrantServerFlow(ClientId clientId, List scopes, 166 | http.Client client, this.userPrompt) 167 | : super(clientId, scopes, client); 168 | 169 | Future run() async { 170 | HttpServer server = await HttpServer.bind('localhost', 0); 171 | 172 | try { 173 | var port = server.port; 174 | var redirectionUri = 'http://localhost:$port'; 175 | var state = 'authcodestate${new DateTime.now().millisecondsSinceEpoch}'; 176 | 177 | // Prompt user and wait until he goes to URL and the google authorization 178 | // server calls back to our locally running HTTP server. 179 | userPrompt(_authenticationUri(redirectionUri, state: state)); 180 | 181 | var request = await server.first; 182 | var uri = request.uri; 183 | 184 | try { 185 | var returnedState = uri.queryParameters['state']; 186 | var code = uri.queryParameters['code']; 187 | var error = uri.queryParameters['error']; 188 | 189 | if (request.method != 'GET') { 190 | throw new Exception('Invalid response from server ' 191 | '(expected GET request callback, got: ${request.method}).'); 192 | } 193 | 194 | if (state != returnedState) { 195 | throw new Exception( 196 | 'Invalid response from server (state did not match).'); 197 | } 198 | 199 | if (error != null) { 200 | throw new UserConsentException( 201 | 'Error occured while obtaining access credentials: $error'); 202 | } 203 | 204 | if (code == null || code == '') { 205 | throw new Exception( 206 | 'Invalid response from server (no auth code transmitted).'); 207 | } 208 | var credentials = 209 | await _obtainAccessCredentialsUsingCode(code, redirectionUri); 210 | 211 | // TODO: We could introduce a user-defined redirect page. 212 | request.response 213 | ..statusCode = 200 214 | ..headers.set('content-type', 'text/html; charset=UTF-8') 215 | ..write(''' 216 | 217 | 218 | 219 | 220 | 221 | Authorization successful. 222 | 223 | 224 | 225 |

Application has successfully obtained access credentials

226 |

This window can be closed now.

227 | 228 | '''); 229 | await request.response.close(); 230 | return credentials; 231 | } catch (e) { 232 | request.response.statusCode = 500; 233 | await request.response.close().catchError((_) {}); 234 | rethrow; 235 | } 236 | } finally { 237 | await server.close(); 238 | } 239 | } 240 | } 241 | 242 | /// Runs an oauth2 authorization code grant flow using manual Copy&Paste. 243 | /// 244 | /// This class is able to run an oauth2 authorization flow. It takes a user 245 | /// supplied function which will be called with an URI. The user is expected 246 | /// to navigate to that URI and to grant access to the client. 247 | /// 248 | /// Google will give the resource owner a code. The user supplied function needs 249 | /// to complete with that code. 250 | /// 251 | /// The authorization code will then be used to obtain access credentials. 252 | class AuthorizationCodeGrantManualFlow 253 | extends AuthorizationCodeGrantAbstractFlow { 254 | final PromptUserForConsentManual userPrompt; 255 | 256 | AuthorizationCodeGrantManualFlow(ClientId clientId, List scopes, 257 | http.Client client, this.userPrompt) 258 | : super(clientId, scopes, client); 259 | 260 | Future run() async { 261 | var redirectionUri = 'urn:ietf:wg:oauth:2.0:oob'; 262 | 263 | // Prompt user and wait until he goes to URL and copy&pastes the auth code 264 | // in. 265 | var code = await userPrompt(_authenticationUri(redirectionUri)); 266 | // Use code to obtain credentials 267 | return _obtainAccessCredentialsUsingCode(code, redirectionUri); 268 | } 269 | } 270 | 271 | // TODO: Server app flow is missing here. 272 | -------------------------------------------------------------------------------- /lib/src/oauth2_flows/implicit.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.implicit_gapi_flow; 6 | 7 | import "dart:async"; 8 | import 'dart:html' as html; 9 | import "dart:js" as js; 10 | 11 | import '../../auth.dart'; 12 | import '../utils.dart'; 13 | 14 | // This will be overridden by tests. 15 | String gapiUrl = 'https://apis.google.com/js/client.js'; 16 | 17 | // According to the CSP3 spec a nonce must be a valid base64 string. 18 | // https://w3c.github.io/webappsec-csp/#grammardef-base64-value 19 | final _noncePattern = new RegExp('^[\\w+\/_-]+[=]{0,2}\$'); 20 | 21 | /// This class performs the implicit browser-based oauth2 flow. 22 | /// 23 | /// It has to be used in two steps: 24 | /// 25 | /// 1. First call initialize() and wait until the Future completes successfully 26 | /// - loads the 'gapi' JavaScript library into the current document 27 | /// - wait until the library signals it is ready 28 | /// 29 | /// 2. Call login() as often as needed. 30 | /// - will call the 'gapi' JavaScript lib to trigger an oauth2 browser flow 31 | /// => This might create a popup which asks the user for consent. 32 | /// - will wait until the flow is completed (successfully or not) 33 | /// => Completes with AccessToken or an Exception. 34 | /// 3. Call loginHybrid() as often as needed. 35 | /// - will call the 'gapi' JavaScript lib to trigger an oauth2 browser flow 36 | /// => This might create a popup which asks the user for consent. 37 | /// - will wait until the flow is completed (successfully or not) 38 | /// => Completes with a tuple [AccessCredentials cred, String authCode] 39 | /// or an Exception. 40 | class ImplicitFlow { 41 | static const CallbackTimeout = const Duration(seconds: 20); 42 | 43 | final String _clientId; 44 | final List _scopes; 45 | 46 | /// The pending result of an earlier call to [initialize], if any. 47 | /// 48 | /// There can be multiple [ImplicitFlow] objects in an application, 49 | /// but the gapi JS library should only ever be loaded once. If 50 | /// it's called again while a previous initialization is still pending, 51 | /// this will be returned. 52 | static Future? _pendingInitialization; 53 | 54 | ImplicitFlow(this._clientId, this._scopes); 55 | 56 | /// Readies the flow for calls to [login] by loading the 'gapi' 57 | /// JavaScript library, or returning the [Future] of a pending 58 | /// initialization if any object has called this method already. 59 | Future initialize() { 60 | if (_pendingInitialization != null) { 61 | return _pendingInitialization!; 62 | } 63 | 64 | var completer = new Completer(); 65 | 66 | var timeout = new Timer(CallbackTimeout, () { 67 | _pendingInitialization = null; 68 | completer.completeError(new Exception( 69 | 'Timed out while waiting for the gapi.auth library to load.')); 70 | }); 71 | 72 | js.context['dartGapiLoaded'] = () { 73 | timeout.cancel(); 74 | try { 75 | var gapi = js.context['gapi']['auth']; 76 | try { 77 | gapi.callMethod('init', [ 78 | () { 79 | completer.complete(); 80 | } 81 | ]); 82 | } on NoSuchMethodError { 83 | throw new StateError('gapi.auth not loaded.'); 84 | } 85 | } catch (error, stack) { 86 | _pendingInitialization = null; 87 | if (!completer.isCompleted) { 88 | completer.completeError(error, stack); 89 | } 90 | } 91 | }; 92 | 93 | var script = _createScript(); 94 | script.src = '${gapiUrl}?onload=dartGapiLoaded'; 95 | script.onError.first.then((errorEvent) { 96 | timeout.cancel(); 97 | _pendingInitialization = null; 98 | if (!completer.isCompleted) { 99 | // script loading errors can still happen after timeouts 100 | completer.completeError(new Exception('Failed to load gapi library.')); 101 | } 102 | }); 103 | html.document.body!.append(script); 104 | 105 | _pendingInitialization = completer.future; 106 | return completer.future; 107 | } 108 | 109 | Future loginHybrid( 110 | {bool force: false, bool immediate: false, String? loginHint}) => 111 | _login(force, immediate, true, loginHint, null); 112 | 113 | Future login( 114 | {bool force: false, 115 | bool immediate: false, 116 | String? loginHint, 117 | List? responseTypes}) async { 118 | return (await _login(force, immediate, false, loginHint, responseTypes)) 119 | .credential; 120 | } 121 | 122 | // Completes with either credentials or a tuple of credentials and authCode. 123 | // hybrid => [AccessCredentials credentials, String authCode] 124 | // !hybrid => AccessCredentials 125 | // 126 | // Alternatively, the response types can be set directly if `hybrid` is not 127 | // set to `true`. 128 | Future _login(bool force, bool immediate, bool hybrid, 129 | String? loginHint, List? responseTypes) { 130 | assert(hybrid != true || responseTypes?.isNotEmpty != true); 131 | 132 | var completer = new Completer(); 133 | 134 | var gapi = js.context['gapi']['auth']; 135 | 136 | var json = { 137 | 'client_id': _clientId, 138 | 'immediate': immediate, 139 | 'approval_prompt': force ? 'force' : 'auto', 140 | 'response_type': responseTypes?.isNotEmpty == true 141 | ? responseTypes! 142 | .map((responseType) => _responseTypeToString(responseType)) 143 | .join(' ') 144 | : hybrid 145 | ? 'code token' 146 | : 'token', 147 | 'scope': _scopes.join(' '), 148 | 'access_type': hybrid ? 'offline' : 'online', 149 | }; 150 | 151 | if (loginHint != null) { 152 | json['login_hint'] = loginHint; 153 | } 154 | 155 | gapi.callMethod('authorize', [ 156 | new js.JsObject.jsify(json), 157 | (jsTokenObject) { 158 | var tokenType = jsTokenObject['token_type']; 159 | var token = jsTokenObject['access_token']; 160 | var expiresInRaw = jsTokenObject['expires_in']; 161 | var code = jsTokenObject['code']; 162 | var error = jsTokenObject['error']; 163 | var idToken = jsTokenObject['id_token']; 164 | 165 | var expiresIn; 166 | if (expiresInRaw is String) { 167 | expiresIn = int.parse(expiresInRaw); 168 | } 169 | if (error != null) { 170 | completer.completeError( 171 | new UserConsentException('Failed to get user consent: $error.')); 172 | } else if (token == null || 173 | expiresIn is! int || 174 | tokenType != 'Bearer') { 175 | completer.completeError(new Exception( 176 | 'Failed to obtain user consent. Invalid server response.')); 177 | } else if (responseTypes?.contains(ResponseType.idToken) == true && 178 | idToken?.isNotEmpty != true) { 179 | completer.completeError( 180 | new Exception('Expected to get id_token, but did not.')); 181 | } else { 182 | var accessToken = 183 | new AccessToken('Bearer', token, expiryDate(expiresIn)); 184 | var credentials = new AccessCredentials(accessToken, null, _scopes, 185 | idToken: idToken); 186 | 187 | if (hybrid) { 188 | if (code == null) { 189 | completer.completeError(new Exception('Expected to get auth code ' 190 | 'from server in hybrid flow, but did not.')); 191 | return; 192 | } 193 | completer.complete(new LoginResult(credentials, code: code)); 194 | } else { 195 | completer.complete(new LoginResult(credentials)); 196 | } 197 | } 198 | } 199 | ]); 200 | 201 | return completer.future; 202 | } 203 | } 204 | 205 | class LoginResult { 206 | final AccessCredentials credential; 207 | final String? code; 208 | 209 | LoginResult(this.credential, {this.code}); 210 | } 211 | 212 | /// Convert [responseType] to string value expected by `gapi.auth.authorize`. 213 | String _responseTypeToString(ResponseType responseType) { 214 | String result; 215 | 216 | switch (responseType) { 217 | case ResponseType.code: 218 | result = 'code'; 219 | break; 220 | 221 | case ResponseType.idToken: 222 | result = 'id_token'; 223 | break; 224 | 225 | case ResponseType.permission: 226 | result = 'permission'; 227 | break; 228 | 229 | case ResponseType.token: 230 | result = 'token'; 231 | break; 232 | 233 | default: 234 | throw ArgumentError('Unknown response type: $responseType'); 235 | } 236 | 237 | return result; 238 | } 239 | 240 | /// Creates a script that will run properly when strict CSP is enforced. 241 | /// 242 | /// More specifically, the script has the correct `nonce` value set. 243 | final _ScriptFactory _createScript = (() { 244 | final nonce = _getNonce(); 245 | if (nonce == null) return () => new html.ScriptElement(); 246 | 247 | return () => new html.ScriptElement()..nonce = nonce; 248 | })(); 249 | 250 | typedef html.ScriptElement _ScriptFactory(); 251 | 252 | /// Returns CSP nonce, if set for any script tag. 253 | String? _getNonce({html.Window? window}) { 254 | final currentWindow = window ?? html.window; 255 | final elements = currentWindow.document.querySelectorAll('script'); 256 | for (final element in elements) { 257 | final nonceValue = 258 | (element as html.HtmlElement).nonce ?? element.attributes['nonce']; 259 | if (nonceValue != null && _noncePattern.hasMatch(nonceValue)) { 260 | return nonceValue; 261 | } 262 | } 263 | return null; 264 | } 265 | -------------------------------------------------------------------------------- /lib/src/oauth2_flows/jwt.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 jwt_token_generator; 6 | 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | 10 | import 'package:http/http.dart' as http; 11 | 12 | import '../../auth.dart'; 13 | import '../crypto/rsa.dart'; 14 | import '../crypto/rsa_sign.dart'; 15 | import '../http_client_base.dart'; 16 | import '../utils.dart'; 17 | 18 | class JwtFlow { 19 | // All details are described at: 20 | // https://developers.google.com/accounts/docs/OAuth2ServiceAccount 21 | // JSON Web Signature (JWS) requires signing a string with a private key. 22 | 23 | static const GOOGLE_OAUTH2_TOKEN_URL = 24 | 'https://accounts.google.com/o/oauth2/token'; 25 | 26 | final String _clientEmail; 27 | final RS256Signer _signer; 28 | final List _scopes; 29 | final String? _user; 30 | final http.Client _client; 31 | 32 | JwtFlow(this._clientEmail, RSAPrivateKey key, this._user, this._scopes, 33 | this._client) 34 | : _signer = new RS256Signer(key); 35 | 36 | Future run() async { 37 | int timestamp = new DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000 - 38 | MAX_EXPECTED_TIMEDIFF_IN_SECONDS; 39 | 40 | jwtHeader() => {"alg": "RS256", "typ": "JWT"}; 41 | 42 | jwtClaimSet() { 43 | var claimSet = { 44 | 'iss': _clientEmail, 45 | 'scope': _scopes.join(' '), 46 | 'aud': GOOGLE_OAUTH2_TOKEN_URL, 47 | 'exp': timestamp + 3600, 48 | 'iat': timestamp, 49 | }; 50 | if (_user != null) claimSet['sub'] = _user!; 51 | return claimSet; 52 | } 53 | 54 | var jwtHeaderBase64 = _base64url(ascii.encode(jsonEncode(jwtHeader()))); 55 | var jwtClaimSetBase64 = _base64url(utf8.encode(jsonEncode(jwtClaimSet()))); 56 | var jwtSignatureInput = '$jwtHeaderBase64.$jwtClaimSetBase64'; 57 | var jwtSignatureInputInBytes = ascii.encode(jwtSignatureInput); 58 | 59 | var signature = _signer.sign(jwtSignatureInputInBytes); 60 | var jwt = "$jwtSignatureInput.${_base64url(signature)}"; 61 | 62 | var uri = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; 63 | var requestParameters = 'grant_type=${Uri.encodeComponent(uri)}&' 64 | 'assertion=${Uri.encodeComponent(jwt)}'; 65 | 66 | var body = new Stream>.fromIterable( 67 | >[utf8.encode(requestParameters)]); 68 | var request = 69 | new RequestImpl('POST', Uri.parse(GOOGLE_OAUTH2_TOKEN_URL), body); 70 | request.headers['content-type'] = CONTENT_TYPE_URLENCODED; 71 | 72 | var httpResponse = await _client.send(request); 73 | var response = await httpResponse.stream 74 | .transform(utf8.decoder) 75 | .transform(json.decoder) 76 | .first as Map; 77 | var tokenType = response['token_type']; 78 | var token = response['access_token']; 79 | var expiresIn = response['expires_in']; 80 | var error = response['error']; 81 | 82 | if (httpResponse.statusCode != 200 && error != null) { 83 | throw new Exception('Unable to obtain credentials. Error: $error.'); 84 | } 85 | 86 | if (tokenType != 'Bearer' || token == null || expiresIn is! int) { 87 | throw new Exception( 88 | 'Unable to obtain credentials. Invalid response from server.'); 89 | } 90 | var accessToken = new AccessToken(tokenType, token, expiryDate(expiresIn)); 91 | return new AccessCredentials(accessToken, null, _scopes); 92 | } 93 | 94 | String _base64url(List bytes) { 95 | return base64Url.encode(bytes).replaceAll('=', ''); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/oauth2_flows/metadata_server.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.metadata_server_flow; 6 | 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | import 'dart:io'; 10 | 11 | import 'package:http/http.dart' as http; 12 | 13 | import '../../auth.dart'; 14 | import '../utils.dart'; 15 | 16 | /// Obtains access credentials form the metadata server. 17 | /// 18 | /// Using this class assumes that the current program is running a 19 | /// ComputeEngine VM. It will retrieve the current access token from the 20 | /// metadata server, looking first for one set in the environment under 21 | /// `$GCE_METADATA_HOST`. 22 | class MetadataServerAuthorizationFlow { 23 | static const _HEADERS = const {'Metadata-Flavor': 'Google'}; 24 | static const _SERVICE_ACCOUNT_URL_INFIX = 25 | 'computeMetadata/v1/instance/service-accounts'; 26 | static const _DEFAULT_METADATA_HOST = "metadata"; 27 | static const _GCE_METADATA_HOST_ENV_VAR = "GCE_METADATA_HOST"; 28 | 29 | final String email; 30 | final Uri _scopesUrl; 31 | final Uri _tokenUrl; 32 | final http.Client _client; 33 | 34 | factory MetadataServerAuthorizationFlow(http.Client client, 35 | {String email: 'default'}) { 36 | var encodedEmail = Uri.encodeComponent(email); 37 | 38 | final metadataHost = Platform.environment[_GCE_METADATA_HOST_ENV_VAR] ?? 39 | _DEFAULT_METADATA_HOST; 40 | final serviceAccountPrefix = 41 | "http://$metadataHost/$_SERVICE_ACCOUNT_URL_INFIX"; 42 | 43 | var scopesUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/scopes'); 44 | var tokenUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/token'); 45 | return new MetadataServerAuthorizationFlow._( 46 | client, email, scopesUrl, tokenUrl); 47 | } 48 | 49 | MetadataServerAuthorizationFlow._( 50 | this._client, this.email, this._scopesUrl, this._tokenUrl); 51 | 52 | Future run() async { 53 | final results = await Future.wait([_getToken(), _getScopes()]); 54 | final Map token = results.first as Map; 55 | final String scopesString = results.last as String; 56 | 57 | var json = token; 58 | var scopes = scopesString 59 | .replaceAll('\n', ' ') 60 | .split(' ') 61 | .where((part) => part.length > 0) 62 | .toList(); 63 | 64 | var type = json['token_type']; 65 | var accessToken = json['access_token']; 66 | var expiresIn = json['expires_in']; 67 | var error = json['error']; 68 | 69 | if (error != null) { 70 | throw new Exception('Error while obtaining credentials from metadata ' 71 | 'server. Error message: $error.'); 72 | } 73 | 74 | if (type != 'Bearer' || accessToken == null || expiresIn is! int) { 75 | throw new Exception('Invalid response from metadata server.'); 76 | } 77 | 78 | return new AccessCredentials( 79 | new AccessToken(type, accessToken, expiryDate(expiresIn)), 80 | null, 81 | scopes); 82 | } 83 | 84 | Future _getToken() async { 85 | var response = await _client.get(_tokenUrl, headers: _HEADERS); 86 | return jsonDecode(response.body); 87 | } 88 | 89 | Future _getScopes() async { 90 | var response = await _client.get(_scopesUrl, headers: _HEADERS); 91 | return response.body; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/src/typedefs.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.typedefs; 6 | 7 | /// Function for directing the user or it's user-agent to [uri]. 8 | /// 9 | /// The user is required to go to [uri] and either approve or decline the 10 | /// application's request for access resources on his behalf. 11 | typedef void PromptUserForConsent(String uri); 12 | 13 | /// Function for directing the user or it's user-agent to [uri]. 14 | /// 15 | /// The user is required to go to [uri] and either approve or decline the 16 | /// application's request for access resources on his behalf. 17 | /// 18 | /// The user will be given an authorization code. This function should complete 19 | /// with this authorization code. If the user declined to give access this 20 | /// function should complete with an error. 21 | typedef Future PromptUserForConsentManual(String uri); 22 | -------------------------------------------------------------------------------- /lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.utils; 6 | 7 | /// Due to differences of clock speed, network latency, etc. we 8 | /// will shorten expiry dates by 20 seconds. 9 | const MAX_EXPECTED_TIMEDIFF_IN_SECONDS = 20; 10 | 11 | /// Constructs a [DateTime] which is [seconds] seconds from now with 12 | /// an offset of [MAX_EXPECTED_TIMEDIFF_IN_SECONDS]. Result is UTC time. 13 | DateTime expiryDate(int seconds) { 14 | return new DateTime.now() 15 | .toUtc() 16 | .add(new Duration(seconds: seconds - MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 17 | } 18 | 19 | /// Constant for the 'application/x-www-form-urlencoded' content type 20 | const CONTENT_TYPE_URLENCODED = 21 | 'application/x-www-form-urlencoded; charset=utf-8'; 22 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: googleapis_auth 2 | version: 0.2.12+2-dev 3 | author: Dart Team 4 | description: Obtain Access credentials for Google services using OAuth 2.0 5 | homepage: https://github.com/dart-lang/googleapis_auth 6 | environment: 7 | sdk: '>=2.12.0-0 <3.0.0' 8 | 9 | dependencies: 10 | crypto: ^3.0.0-nullsafety.0 11 | # Not published yet. 12 | http: 13 | git: 14 | url: git://github.com/dart-lang/http.git 15 | ref: 3845753a54624b070828cb3eff7a6c2a4e046cfb 16 | 17 | dev_dependencies: 18 | test: ^1.16.0-nullsafety.9 19 | 20 | dependency_overrides: 21 | # Needs bump in `packge:test`. 22 | crypto: ^3.0.0-nullsafety.0 23 | http_parser: ^4.0.0-nullsafety 24 | -------------------------------------------------------------------------------- /test/adc_test.dart: -------------------------------------------------------------------------------- 1 | @TestOn("vm") 2 | library googleapis_auth.adc_test; 3 | 4 | import 'dart:io'; 5 | import 'dart:convert'; 6 | import 'package:googleapis_auth/src/adc_utils.dart' 7 | show fromApplicationsCredentialsFile; 8 | import 'package:http/http.dart'; 9 | import 'package:test/test.dart'; 10 | 11 | import 'test_utils.dart'; 12 | 13 | main() { 14 | test('fromApplicationsCredentialsFile', () async { 15 | final tmp = await Directory.systemTemp.createTemp('googleapis_auth-test'); 16 | try { 17 | final credsFile = File.fromUri(tmp.uri.resolve('creds.json')); 18 | await credsFile.writeAsString(json.encode({ 19 | "client_id": "id", 20 | "client_secret": "secret", 21 | "refresh_token": "refresh", 22 | "type": "authorized_user" 23 | })); 24 | final c = await fromApplicationsCredentialsFile( 25 | credsFile, 26 | 'test-credentials-file', 27 | [], 28 | mockClient((Request request) async { 29 | final url = request.url.toString(); 30 | if (url == 'https://accounts.google.com/o/oauth2/token') { 31 | expect(request.method, equals('POST')); 32 | expect( 33 | request.body, 34 | equals('client_id=id&' 35 | 'client_secret=secret&' 36 | 'refresh_token=refresh&' 37 | 'grant_type=refresh_token')); 38 | var body = jsonEncode({ 39 | 'token_type': 'Bearer', 40 | 'access_token': 'atoken', 41 | 'expires_in': 3600, 42 | }); 43 | return new Response(body, 200, headers: _jsonContentType); 44 | } 45 | if (url == 'https://storage.googleapis.com/b/bucket/o/obj') { 46 | expect(request.method, equals('GET')); 47 | expect(request.headers['Authorization'], equals('Bearer atoken')); 48 | expect(request.headers['X-Goog-User-Project'], isNull); 49 | return new Response('hello world', 200); 50 | } 51 | return Response('bad', 404); 52 | }, expectClose: false), 53 | ); 54 | expect(c.credentials.accessToken.data, equals('atoken')); 55 | 56 | final r = 57 | await c.get(Uri.https('storage.googleapis.com', '/b/bucket/o/obj')); 58 | expect(r.statusCode, equals(200)); 59 | expect(r.body, equals('hello world')); 60 | 61 | c.close(); 62 | } finally { 63 | await tmp.delete(recursive: true); 64 | } 65 | }); 66 | 67 | test('fromApplicationsCredentialsFile w. quota_project_id', () async { 68 | final tmp = await Directory.systemTemp.createTemp('googleapis_auth-test'); 69 | try { 70 | final credsFile = File.fromUri(tmp.uri.resolve('creds.json')); 71 | await credsFile.writeAsString(json.encode({ 72 | "client_id": "id", 73 | "client_secret": "secret", 74 | "refresh_token": "refresh", 75 | "type": "authorized_user", 76 | "quota_project_id": "project" 77 | })); 78 | final c = await fromApplicationsCredentialsFile( 79 | credsFile, 80 | 'test-credentials-file', 81 | [], 82 | mockClient((Request request) async { 83 | final url = request.url.toString(); 84 | if (url == 'https://accounts.google.com/o/oauth2/token') { 85 | expect(request.method, equals('POST')); 86 | expect( 87 | request.body, 88 | equals('client_id=id&' 89 | 'client_secret=secret&' 90 | 'refresh_token=refresh&' 91 | 'grant_type=refresh_token')); 92 | var body = jsonEncode({ 93 | 'token_type': 'Bearer', 94 | 'access_token': 'atoken', 95 | 'expires_in': 3600, 96 | }); 97 | return new Response(body, 200, headers: _jsonContentType); 98 | } 99 | if (url == 'https://storage.googleapis.com/b/bucket/o/obj') { 100 | expect(request.method, equals('GET')); 101 | expect(request.headers['Authorization'], equals('Bearer atoken')); 102 | expect(request.headers['X-Goog-User-Project'], equals('project')); 103 | return new Response('hello world', 200); 104 | } 105 | return Response('bad', 404); 106 | }, expectClose: false), 107 | ); 108 | expect(c.credentials.accessToken.data, equals('atoken')); 109 | 110 | final r = 111 | await c.get(Uri.https('storage.googleapis.com', '/b/bucket/o/obj')); 112 | expect(r.statusCode, equals(200)); 113 | expect(r.body, equals('hello world')); 114 | 115 | c.close(); 116 | } finally { 117 | await tmp.delete(recursive: true); 118 | } 119 | }); 120 | } 121 | 122 | final _jsonContentType = const {'content-type': 'application/json'}; 123 | -------------------------------------------------------------------------------- /test/crypto/asn1_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.test.asn1_test; 6 | 7 | import 'dart:typed_data'; 8 | 9 | import 'package:googleapis_auth/src/crypto/asn1.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | main() { 13 | expectArgumentError(List bytes) { 14 | expect(() => ASN1Parser.parse(new Uint8List.fromList(bytes)), 15 | throwsA(isArgumentError)); 16 | } 17 | 18 | invalidLenTest(int tagBytes) { 19 | test('invalid-len', () { 20 | expectArgumentError([tagBytes]); 21 | expectArgumentError([tagBytes, 0x07]); 22 | expectArgumentError([tagBytes, 0x82]); 23 | expectArgumentError([tagBytes, 0x82, 1]); 24 | expectArgumentError([tagBytes, 0x01, 1, 2, 3, 4]); 25 | }); 26 | } 27 | 28 | group('asn1-parser', () { 29 | group('sequence', () { 30 | test('empty', () { 31 | var sequenceBytes = [ASN1Parser.SEQUENCE_TAG, 0]; 32 | var sequence = ASN1Parser.parse(new Uint8List.fromList(sequenceBytes)); 33 | expect(sequence is ASN1Sequence, isTrue); 34 | expect((sequence as ASN1Sequence).objects, isEmpty); 35 | }); 36 | 37 | test('one-element', () { 38 | var sequenceBytes = [ 39 | ASN1Parser.SEQUENCE_TAG, 40 | 1, 41 | ASN1Parser.NULL_TAG, 42 | 0 43 | ]; 44 | var sequence = ASN1Parser.parse(new Uint8List.fromList(sequenceBytes)); 45 | expect(sequence is ASN1Sequence, isTrue); 46 | expect((sequence as ASN1Sequence).objects, hasLength(1)); 47 | expect(sequence.objects[0] is ASN1Null, isTrue); 48 | }); 49 | 50 | test('many-elements', () { 51 | var sequenceBytes = [ASN1Parser.SEQUENCE_TAG, 0x82, 0x01, 0x00]; 52 | for (int i = 0; i < 128; i++) { 53 | sequenceBytes.addAll([ASN1Parser.NULL_TAG, 0]); 54 | } 55 | 56 | var sequence = ASN1Parser.parse(new Uint8List.fromList(sequenceBytes)); 57 | expect(sequence is ASN1Sequence, isTrue); 58 | expect((sequence as ASN1Sequence).objects.length, equals(128)); 59 | for (int i = 0; i < 128; i++) { 60 | expect(sequence.objects[i] is ASN1Null, isTrue); 61 | } 62 | }); 63 | 64 | invalidLenTest(ASN1Parser.SEQUENCE_TAG); 65 | }); 66 | 67 | group('integer', () { 68 | test('small', () { 69 | for (int i = 0; i < 256; i++) { 70 | var integerBytes = [ASN1Parser.INTEGER_TAG, 1, i]; 71 | var integer = ASN1Parser.parse(new Uint8List.fromList(integerBytes)) 72 | as ASN1Integer; 73 | expect(integer.integer, new BigInt.from(i)); 74 | } 75 | }); 76 | 77 | test('multi-byte', () { 78 | var integerBytes = [ASN1Parser.INTEGER_TAG, 3, 1, 2, 3]; 79 | var integer = ASN1Parser.parse(new Uint8List.fromList(integerBytes)); 80 | expect(integer is ASN1Integer, isTrue); 81 | expect((integer as ASN1Integer).integer, new BigInt.from(0x010203)); 82 | }); 83 | 84 | invalidLenTest(ASN1Parser.INTEGER_TAG); 85 | }); 86 | 87 | group('octet-string', () { 88 | test('small', () { 89 | var octetStringBytes = [ASN1Parser.OCTET_STRING_TAG, 3, 1, 2, 3]; 90 | var octetString = 91 | ASN1Parser.parse(new Uint8List.fromList(octetStringBytes)); 92 | expect(octetString is ASN1OctetString, isTrue); 93 | expect((octetString as ASN1OctetString).bytes, equals([1, 2, 3])); 94 | }); 95 | 96 | test('large', () { 97 | var octetStringBytes = [ASN1Parser.OCTET_STRING_TAG, 0x82, 0x01, 0x00]; 98 | for (int i = 0; i < 256; i++) octetStringBytes.add(i % 256); 99 | 100 | var octetString = 101 | ASN1Parser.parse(new Uint8List.fromList(octetStringBytes)); 102 | expect(octetString is ASN1OctetString, isTrue); 103 | ASN1OctetString castedOctetString = octetString as ASN1OctetString; 104 | for (int i = 0; i < 256; i++) { 105 | expect(castedOctetString.bytes[i], equals((i % 256))); 106 | } 107 | }); 108 | 109 | invalidLenTest(ASN1Parser.OCTET_STRING_TAG); 110 | }); 111 | 112 | group('oid', () { 113 | // NOTE: Currently the oid is parsed as normal bytes, so we don't validate 114 | // the oid structure. 115 | test('small', () { 116 | var objIdBytes = [ASN1Parser.OBJECT_ID_TAG, 3, 1, 2, 3]; 117 | var objId = ASN1Parser.parse(new Uint8List.fromList(objIdBytes)); 118 | expect(objId is ASN1ObjectIdentifier, isTrue); 119 | expect((objId as ASN1ObjectIdentifier).bytes, equals([1, 2, 3])); 120 | }); 121 | 122 | test('large', () { 123 | var objIdBytes = [ASN1Parser.OBJECT_ID_TAG, 0x82, 0x01, 0x00]; 124 | for (int i = 0; i < 256; i++) objIdBytes.add(i % 256); 125 | 126 | var objId = ASN1Parser.parse(new Uint8List.fromList(objIdBytes)); 127 | expect(objId is ASN1ObjectIdentifier, isTrue); 128 | ASN1ObjectIdentifier castedObjId = objId as ASN1ObjectIdentifier; 129 | for (int i = 0; i < 256; i++) { 130 | expect(castedObjId.bytes[i], equals((i % 256))); 131 | } 132 | }); 133 | 134 | invalidLenTest(ASN1Parser.OBJECT_ID_TAG); 135 | }); 136 | }); 137 | 138 | test('null', () { 139 | var objId = 140 | ASN1Parser.parse(new Uint8List.fromList([ASN1Parser.NULL_TAG, 0x00])); 141 | expect(objId is ASN1Null, isTrue); 142 | 143 | expectArgumentError([ASN1Parser.NULL_TAG]); 144 | expectArgumentError([ASN1Parser.NULL_TAG, 0x01]); 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /test/crypto/pem_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.test.pem_test; 6 | 7 | import 'package:googleapis_auth/src/crypto/pem.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | import '../test_utils.dart'; 11 | 12 | main() { 13 | group('pem', () { 14 | test('null', () { 15 | expect(() => keyFromString(''), throwsA(isArgumentError)); 16 | }); 17 | 18 | test('pem--key-from-string', () { 19 | var key = keyFromString(TestPrivateKeyString); 20 | expect( 21 | key.p, 22 | equals(BigInt.parse( 23 | '170185878019789847607218833833962851295383479739128068911675681859184825725303329240997154492057125840628991571181411414164882361723231273391547091096391845233984484218520948165420605211532206383859989286454330226302062891556391372178426684136261758077913279309249468965000813860343415338472623037185763380093'))); 24 | expect( 25 | key.q, 26 | equals(BigInt.parse( 27 | '136634937867625346722869734066327766542560453705266659651284573193680854438532412351608161985232086174999341126075829838477923122149398705411098928405144549034231120055200290950893136823181693383585861140730929930114638604738429489364496581584222788741142343940831827356789459450282075298628271623617861448279'))); 28 | expect( 29 | key.n, 30 | equals(BigInt.parse( 31 | '23253336869181252005308127869627478511861722018560725538542603352356752658510633204810959681459083455055115233727694253121121138828979138624495569601457246561359553177524606534054439784597124760679930421448728375265700767584567585959695707287695356045087640902894887625471020788794811661755081070077086649519865067918501869783817745592796089436450623267438942174934673417424553992577792939276705103879103955476795626469391055763713456179432199172562526422301070938382514265029982800538033050279129668807032677927531973249309321914500317007151921938466293582589451642241740444272968677617027011566610435323463337709947'))); 32 | expect( 33 | key.d, 34 | equals(BigInt.parse( 35 | '21186554940454261253047269959735660724480631477978821785517431853394668885438560354085051566279884512080977781045029208574826211785037495240030508751426142586201712610225510861978099522679761260199887167944008250970681053969661407950094604171122649803382413195502685962008111346880629170494825836648656453852203519401121722270587408277317819537925146228717860401265662699719826243356610955461998054615517371279631680512102389979478015385709644867750888484550190071229275090881149432467365050794063725847869274512118390103343213000471284707060203072264487986083004823016463235156640750689592865369834958756866148520449'))); 36 | expect(key.e, equals(new BigInt.from(65537))); 37 | expect( 38 | key.dmp1, 39 | equals(BigInt.parse( 40 | '8112374428701702609593842209702915108210293280208677346843383586799722226617751812699316578927727255231777006398991855865405686833748485558923861522271817820635175987589597358267451526325993144989526626651865780047418167954318425419006133348210655541684866328365584952723843668457708310075048817739114161457'))); 41 | expect( 42 | key.dmq1, 43 | equals(BigInt.parse( 44 | '69064888333930830841944331910451194321610695483381427808232052980561601308959263072336597373770287299070802348040252301131546443496698520136006747353055884093824470361301555431744464097251017848208627523520965497274938325818544542688522182250340240209771921627903870254182590341478772425006618460954711021211'))); 45 | expect( 46 | key.coeff, 47 | equals(BigInt.parse( 48 | '16726959063327324857338379758571748557044292252371297447561270320393087678399207080059961434627453370656491757664831584315003981946034135341817305303511530360890203726058358401094205679273808207987503167082629712433452873772120961093571912870024590300080209978748890272607981079166485164486378666155431958545'))); 49 | }); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /test/crypto/rsa_sign_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.test.crypto_rsa_sign_test; 6 | 7 | import 'dart:convert'; 8 | 9 | import 'package:googleapis_auth/src/crypto/rsa_sign.dart'; 10 | import 'package:test/test.dart'; 11 | 12 | import '../test_utils.dart'; 13 | 14 | void main() { 15 | group('rsa-sha256-signer', () { 16 | var signer = new RS256Signer(testPrivateKey); 17 | 18 | // NOTE: 19 | // The signatures can be regenerated via the openssl commandline utility: 20 | // $ cat plaintext | openssl dgst -sha256 -sign key.pem > ciphertext 21 | // e.g. 22 | // $ echo -n "hello world"|openssl dgst -sha256 -sign key.pem|hexdump -v -C 23 | // 00000000 59 9d 6f 81 c1 0f d6 f1 58 46 2d 4d c9 b8 69 1d 24 | // 00000010 b1 e0 e0 26 a4 de 49 d8 4f 5a ac db 81 ab 10 27 25 | // 00000020 3a f4 5a f8 bb da a9 84 be c7 5a fb b9 2e 0a 66 26 | // 00000030 8f 78 d5 cb c9 82 0b 57 36 fc bc 42 1b f5 fa 76 27 | // 00000040 b7 01 4c bc 2d b9 fe 20 55 62 f5 87 8c bc e3 58 28 | // 00000050 a6 c6 8a ef 16 c8 4a 85 01 6e df 05 43 c8 ef 35 29 | // 00000060 37 9f 1b 29 57 eb c7 93 89 75 f5 65 81 0a 6c 8c 30 | // 00000070 44 35 ad 73 89 90 53 42 26 f3 31 a9 06 f1 32 20 31 | // 00000080 48 a3 e1 68 3d 86 67 45 74 19 91 75 c9 28 ca 8b 32 | // 00000090 33 63 ed a2 b1 90 e6 e1 0a 1f 87 ec 02 f8 92 03 33 | // 000000a0 cf 0e 30 49 b0 f1 72 29 a3 9c 2e cc 7c 87 65 11 34 | // 000000b0 1f 38 34 d3 3e fe af 8e 31 f0 10 1f f5 71 dd 90 35 | // 000000c0 f6 c7 ba 5d 10 0c 63 eb a4 3c a5 17 9a 99 52 2d 36 | // 000000d0 b6 27 96 8c e2 44 63 35 1f 04 6f b8 31 e6 d4 47 37 | // 000000e0 31 0d 3c 36 6c bf 14 df dc 2d 53 c7 ca d1 ec 6d 38 | // 000000f0 95 37 2f 86 14 da 6c 04 a1 fd 45 fa 95 e0 04 bf 39 | test('encrypt-hello-world', () { 40 | expect( 41 | signer.sign(ascii.encode('hello world')), 42 | equals([ 43 | 89, 157, 111, 129, 193, 15, 214, 241, 88, 70, 45, 77, 201, 184, //!! 44 | 105, 29, 177, 224, 224, 38, 164, 222, 73, 216, 79, 90, 172, 219, 45 | 129, 171, 16, 39, 58, 244, 90, 248, 187, 218, 169, 132, 190, 199, 46 | 90, 251, 185, 46, 10, 102, 143, 120, 213, 203, 201, 130, 11, 87, 54, 47 | 252, 188, 66, 27, 245, 250, 118, 183, 1, 76, 188, 45, 185, 254, 32, 48 | 85, 98, 245, 135, 140, 188, 227, 88, 166, 198, 138, 239, 22, 49 | 200, 74, 133, 1, 110, 223, 5, 67, 200, 239, 53, 55, 159, 27, 41, 87, 50 | 235, 199, 147, 137, 117, 245, 101, 129, 10, 108, 140, 68, 53, 173, 51 | 115, 137, 144, 83, 66, 38, 243, 49, 169, 6, 241, 50, 32, 72, 163, 52 | 225, 104, 61, 134, 103, 69, 116, 25, 145, 117, 201, 40, 202, 139, 53 | 51, 99, 237, 162, 177, 144, 230, 225, 10, 31, 135, 236, 2, 248, 146, 54 | 3, 207, 14, 48, 73, 176, 241, 114, 41, 163, 156, 46, 204, 124, 135, 55 | 101, 17, 31, 56, 52, 211, 62, 254, 175, 142, 49, 240, 16, 31, 245, 56 | 113, 221, 144, 246, 199, 186, 93, 16, 12, 99, 235, 164, 60, 165, 23, 57 | 154, 153, 82, 45, 182, 39, 150, 140, 226, 68, 99, 53, 31, 4, 111, 58 | 184, 49, 230, 212, 71, 49, 13, 60, 54, 108, 191, 20, 223, 220, 45, 59 | 83, 199, 202, 209, 236, 109, 149, 55, 47, 134, 20, 218, 108, 4, 161, 60 | 253, 69, 250, 149, 224, 4, 191 61 | ])); 62 | }); 63 | 64 | // $ echo -n ""|openssl dgst -sha256 -sign key.pem|hexdump -v -C 65 | test('null-bytes', () { 66 | expect( 67 | signer.sign([]), 68 | equals([ 69 | 113, 99, 2, 245, 156, 215, 253, 172, 157, 46, 126, 165, 174, //!! 70 | 158, 186, 213, 211, 85, 118, 63, 208, 122, 196, 214, 154, 221, 92, 71 | 105, 27, 29, 153, 35, 91, 111, 5, 10, 82, 213, 179, 41, 165, 122, 72 | 227, 145, 217, 108, 249, 153, 116, 80, 140, 238, 158, 140, 142, 118, 73 | 224, 10, 225, 58, 77, 210, 27, 66, 177, 165, 228, 40, 225, 211, 140, 74 | 254, 31, 242, 230, 223, 21, 199, 221, 113, 146, 46, 213, 20, 63, 75 | 148, 140, 144, 245, 105, 193, 124, 206, 235, 191, 252, 138, 155, 76 | 148, 175, 185, 160, 98, 102, 156, 197, 29, 80, 202, 49, 26, 173, 77 | 176, 53, 202, 13, 204, 180, 180, 190, 152, 223, 199, 65, 9, 173, 82, 78 | 167, 12, 244, 127, 141, 8, 103, 155, 213, 2, 53, 83, 179, 157, 101, 79 | 190, 205, 85, 58, 50, 89, 255, 11, 67, 18, 232, 252, 229, 197, 200, 80 | 228, 130, 104, 250, 228, 19, 178, 183, 45, 156, 22, 73, 229, 170, 81 | 163, 179, 116, 21, 149, 31, 81, 253, 100, 132, 46, 216, 143, 134, 82 | 185, 96, 75, 57, 139, 21, 131, 114, 221, 124, 47, 104, 92, 235, 254, 83 | 62, 69, 126, 117, 170, 141, 64, 121, 181, 101, 69, 135, 115, 102, 84 | 74, 157, 233, 127, 139, 14, 79, 137, 156, 248, 117, 114, 205, 142, 85 | 60, 8, 116, 77, 182, 28, 119, 149, 143, 252, 141, 46, 111, 100, 242, 86 | 184, 21, 130, 61, 138, 27, 226, 70, 119, 195, 223, 180, 121 87 | ])); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /test/crypto/rsa_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.test.rsa_test; 6 | 7 | import 'package:googleapis_auth/src/crypto/rsa.dart'; 8 | import 'package:test/test.dart'; 9 | 10 | import '../test_utils.dart'; 11 | 12 | /// 2 << 64 13 | final _bigNumber = BigInt.parse('20000000000000000', radix: 16); 14 | 15 | main() { 16 | group('rsa-algorithm', () { 17 | test('integer-to-bytes', () { 18 | expect(RSAAlgorithm.integer2Bytes(BigInt.one, 1), equals([1])); 19 | expect(RSAAlgorithm.integer2Bytes(_bigNumber, 9), 20 | equals([2, 0, 0, 0, 0, 0, 0, 0, 0])); 21 | expect(RSAAlgorithm.integer2Bytes(_bigNumber, 12), 22 | equals([0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0])); 23 | expect(() => RSAAlgorithm.integer2Bytes(BigInt.zero, 1), 24 | throwsA(isArgumentError)); 25 | }); 26 | 27 | test('bytes-to-integer', () { 28 | expect(RSAAlgorithm.bytes2BigInt([1]), equals(BigInt.one)); 29 | expect( 30 | RSAAlgorithm.bytes2BigInt([2, 0, 0, 0, 0, 0, 0, 0, 0]), _bigNumber); 31 | }); 32 | 33 | test('encrypt', () { 34 | var encryptedData = [ 35 | 155, 24, 116, 247, 12, 118, 240, 206, 240, 138, 136, 193, 3, 73, //!! 36 | 241, 63, 212, 100, 97, 46, 55, 113, 119, 95, 240, 219, 136, 211, 3, 4, 37 | 43, 137, 213, 92, 233, 57, 172, 80, 179, 117, 83, 88, 249, 75, 17, 20, 38 | 195, 51, 25, 97, 248, 217, 41, 117, 55, 63, 5, 252, 42, 133, 82, 73, 52, 39 | 219, 255, 38, 137, 209, 83, 57, 245, 188, 180, 233, 249, 144, 100, 153, 40 | 145, 14, 94, 2, 229, 165, 131, 178, 195, 178, 95, 244, 153, 196, 130, 41 | 39, 158, 143, 98, 181, 223, 184, 68, 198, 201, 203, 89, 15, 41, 185, 42 | 226, 64, 226, 161, 43, 228, 90, 58, 152, 203, 142, 133, 113, 120, 97, 43 | 78, 149, 86, 214, 135, 29, 29, 190, 16, 47, 210, 1, 213, 86, 100, 116, 44 | 187, 11, 255, 224, 6, 6, 206, 60, 138, 24, 179, 245, 248, 200, 45, 167, 45 | 100, 78, 131, 204, 120, 22, 73, 116, 127, 65, 201, 15, 177, 250, 4, 73, 46 | 245, 67, 119, 21, 54, 255, 227, 206, 37, 216, 13, 8, 109, 238, 215, 22, 47 | 63, 163, 155, 33, 148, 254, 113, 17, 68, 65, 48, 82, 43, 240, 249, 87, 48 | 19, 87, 162, 148, 169, 93, 22, 135, 125, 134, 187, 48, 93, 52, 20, 182, 49 | 56, 93, 0, 175, 193, 213, 144, 29, 44, 240, 226, 91, 54, 178, 241, 240, 50 | 85, 53, 148, 172, 138, 107, 131, 14, 157, 183, 137, 46, 130, 51, 233, 51 | 26, 217, 230, 133, 217, 76 52 | ]; 53 | expect( 54 | RSAAlgorithm.encrypt( 55 | testPrivateKey, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 256), 56 | equals(encryptedData)); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/http_client_base_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.http_client_base_test; 6 | 7 | import 'dart:async'; 8 | 9 | import 'package:googleapis_auth/src/http_client_base.dart'; 10 | import 'package:googleapis_auth/src/auth_http_utils.dart'; 11 | import 'package:test/test.dart'; 12 | import 'package:http/http.dart'; 13 | 14 | import 'test_utils.dart'; 15 | 16 | class DelegatingClientImpl extends DelegatingClient { 17 | DelegatingClientImpl(Client base, {required bool closeUnderlyingClient}) 18 | : super(base, closeUnderlyingClient: closeUnderlyingClient); 19 | 20 | Future send(BaseRequest request) => throw 'unsupported'; 21 | } 22 | 23 | final _defaultResponse = Response('', 500); 24 | final _defaultResponseHandler = (Request _) async => _defaultResponse; 25 | 26 | main() { 27 | group('http-utils', () { 28 | group('delegating-client', () { 29 | test('not-close-underlying-client', () { 30 | var mock = mockClient(_defaultResponseHandler, expectClose: false); 31 | new DelegatingClientImpl(mock, closeUnderlyingClient: false).close(); 32 | }); 33 | 34 | test('close-underlying-client', () { 35 | var mock = mockClient(_defaultResponseHandler, expectClose: true); 36 | new DelegatingClientImpl(mock, closeUnderlyingClient: true).close(); 37 | }); 38 | 39 | test('close-several-times', () { 40 | var mock = mockClient(_defaultResponseHandler, expectClose: true); 41 | var delegate = 42 | new DelegatingClientImpl(mock, closeUnderlyingClient: true); 43 | delegate.close(); 44 | expect(() => delegate.close(), throwsA(isStateError)); 45 | }); 46 | }); 47 | 48 | group('refcounted-client', () { 49 | test('not-close-underlying-client', () { 50 | var mock = mockClient(_defaultResponseHandler, expectClose: false); 51 | var client = new RefCountedClient(mock, initialRefCount: 3); 52 | client.close(); 53 | client.close(); 54 | }); 55 | 56 | test('close-underlying-client', () { 57 | var mock = mockClient(_defaultResponseHandler, expectClose: true); 58 | var client = new RefCountedClient(mock, initialRefCount: 3); 59 | client.close(); 60 | client.close(); 61 | client.close(); 62 | }); 63 | 64 | test('acquire-release', () { 65 | var mock = mockClient(_defaultResponseHandler, expectClose: true); 66 | var client = new RefCountedClient(mock, initialRefCount: 1); 67 | client.acquire(); 68 | client.release(); 69 | client.acquire(); 70 | client.release(); 71 | client.release(); 72 | }); 73 | 74 | test('close-several-times', () { 75 | var mock = mockClient(_defaultResponseHandler, expectClose: true); 76 | var client = new RefCountedClient(mock, initialRefCount: 1); 77 | client.close(); 78 | expect(() => client.close(), throwsA(isStateError)); 79 | }); 80 | }); 81 | 82 | group('api-client', () { 83 | var key = 'foo%?bar'; 84 | var keyEncoded = 'key=${Uri.encodeQueryComponent(key)}'; 85 | 86 | RequestImpl request(String url) => new RequestImpl('GET', Uri.parse(url)); 87 | Future responseF() => 88 | new Future.value(new Response.bytes([], 200)); 89 | 90 | test('no-query-string', () { 91 | var mock = mockClient((Request request) { 92 | expect('${request.url}', equals('http://localhost/abc?$keyEncoded')); 93 | return responseF(); 94 | }, expectClose: true); 95 | 96 | var client = new ApiKeyClient(mock, key); 97 | expect(client.send(request('http://localhost/abc')), completes); 98 | client.close(); 99 | }); 100 | 101 | test('with-query-string', () { 102 | var mock = mockClient((Request request) { 103 | expect( 104 | '${request.url}', equals('http://localhost/abc?x&$keyEncoded')); 105 | return responseF(); 106 | }, expectClose: true); 107 | 108 | var client = new ApiKeyClient(mock, key); 109 | expect(client.send(request('http://localhost/abc?x')), completes); 110 | client.close(); 111 | }); 112 | 113 | test('with-existing-key', () { 114 | var mock = mockClient(expectAsync1(_defaultResponseHandler, count: 0), 115 | expectClose: true); 116 | 117 | var client = new ApiKeyClient(mock, key); 118 | expect(client.send(request('http://localhost/abc?key=a')), 119 | throwsException); 120 | client.close(); 121 | }); 122 | }); 123 | 124 | test('non-closing-client', () { 125 | var mock = mockClient(_defaultResponseHandler, expectClose: false); 126 | nonClosingClient(mock).close(); 127 | }); 128 | }); 129 | } 130 | -------------------------------------------------------------------------------- /test/oauth2_flows/auth_code_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.auth_code_test; 6 | 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | import 'dart:io'; 10 | 11 | import 'package:googleapis_auth/auth.dart'; 12 | import 'package:googleapis_auth/src/oauth2_flows/auth_code.dart'; 13 | import 'package:http/http.dart'; 14 | import 'package:test/test.dart'; 15 | 16 | import '../test_utils.dart'; 17 | 18 | typedef Future RequestHandler(Request _); 19 | 20 | main() { 21 | var clientId = new ClientId('id', 'secret'); 22 | var scopes = ['s1', 's2']; 23 | 24 | // Validation + Responses from the authorization server. 25 | 26 | RequestHandler successFullResponse({bool? manual}) { 27 | return (Request request) async { 28 | expect(request.method, equals('POST')); 29 | expect('${request.url}', 30 | equals('https://accounts.google.com/o/oauth2/token')); 31 | expect( 32 | request.headers['content-type']! 33 | .startsWith('application/x-www-form-urlencoded'), 34 | isTrue); 35 | 36 | var pairs = request.body.split('&'); 37 | expect(pairs, hasLength(5)); 38 | expect(pairs[0], equals('grant_type=authorization_code')); 39 | expect(pairs[1], equals('code=mycode')); 40 | expect(pairs[3], equals('client_id=id')); 41 | expect(pairs[4], equals('client_secret=secret')); 42 | if (manual!) { 43 | expect(pairs[2], 44 | equals('redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob')); 45 | } else { 46 | expect(pairs[2], startsWith('redirect_uri=')); 47 | 48 | var url = Uri.parse( 49 | Uri.decodeComponent(pairs[2].substring('redirect_uri='.length))); 50 | expect(url.scheme, equals('http')); 51 | expect(url.host, equals('localhost')); 52 | } 53 | 54 | var result = { 55 | 'token_type': 'Bearer', 56 | 'access_token': 'tokendata', 57 | 'expires_in': 3600, 58 | 'refresh_token': 'my-refresh-token', 59 | 'id_token': 'my-id-token', 60 | }; 61 | return new Response(jsonEncode(result), 200); 62 | }; 63 | } 64 | 65 | Future invalidResponse(Request request) { 66 | // Missing expires_in field! 67 | var result = { 68 | 'token_type': 'Bearer', 69 | 'access_token': 'tokendata', 70 | 'refresh_token': 'my-refresh-token', 71 | 'id_token': 'my-id-token', 72 | }; 73 | return new Future.value(new Response(jsonEncode(result), 200)); 74 | } 75 | 76 | // Validation functions for user prompt and access credentials. 77 | 78 | void validateAccessCredentials(AccessCredentials credentials) { 79 | expect(credentials.accessToken.data, equals('tokendata')); 80 | expect(credentials.accessToken.type, equals('Bearer')); 81 | expect(credentials.scopes, equals(['s1', 's2'])); 82 | expect(credentials.refreshToken, equals('my-refresh-token')); 83 | expect(credentials.idToken, equals('my-id-token')); 84 | expectExpiryOneHourFromNow(credentials.accessToken); 85 | } 86 | 87 | Uri validateUserPromptUri(String url, {bool manual: false}) { 88 | var uri = Uri.parse(url); 89 | expect(uri.scheme, equals('https')); 90 | expect(uri.host, equals('accounts.google.com')); 91 | expect(uri.path, equals('/o/oauth2/auth')); 92 | expect(uri.queryParameters['client_id'], equals(clientId.identifier)); 93 | expect(uri.queryParameters['response_type'], equals('code')); 94 | expect(uri.queryParameters['scope'], equals('s1 s2')); 95 | expect(uri.queryParameters['redirect_uri'], isNotNull); 96 | 97 | var redirectUri = Uri.parse(uri.queryParameters['redirect_uri']!); 98 | 99 | if (manual) { 100 | expect('$redirectUri', equals('urn:ietf:wg:oauth:2.0:oob')); 101 | } else { 102 | expect(uri.queryParameters['state'], isNotNull); 103 | expect(redirectUri.scheme, equals('http')); 104 | expect(redirectUri.host, equals('localhost')); 105 | } 106 | 107 | return redirectUri; 108 | } 109 | 110 | group('authorization-code-flow', () { 111 | group('manual-copy-paste', () { 112 | Future manualUserPrompt(String url) { 113 | validateUserPromptUri(url, manual: true); 114 | return new Future.value('mycode'); 115 | } 116 | 117 | test('successfull', () async { 118 | var flow = new AuthorizationCodeGrantManualFlow( 119 | clientId, 120 | scopes, 121 | mockClient(successFullResponse(manual: true), expectClose: false), 122 | manualUserPrompt); 123 | validateAccessCredentials(await flow.run()); 124 | }); 125 | 126 | test('user-exception', () { 127 | // We use a TransportException here for convenience. 128 | Future manualUserPromptError(String url) { 129 | return new Future.error(new TransportException()); 130 | } 131 | 132 | var flow = new AuthorizationCodeGrantManualFlow( 133 | clientId, 134 | scopes, 135 | mockClient(successFullResponse(manual: true), expectClose: false), 136 | manualUserPromptError); 137 | expect(flow.run(), throwsA(isTransportException)); 138 | }); 139 | 140 | test('transport-exception', () { 141 | var flow = new AuthorizationCodeGrantManualFlow( 142 | clientId, scopes, transportFailure, manualUserPrompt); 143 | expect(flow.run(), throwsA(isTransportException)); 144 | }); 145 | 146 | test('invalid-server-response', () { 147 | var flow = new AuthorizationCodeGrantManualFlow(clientId, scopes, 148 | mockClient(invalidResponse, expectClose: false), manualUserPrompt); 149 | expect(flow.run(), throwsA(isException)); 150 | }); 151 | }); 152 | 153 | group('http-server', () { 154 | void callRedirectionEndpoint(Uri authCodeCall) { 155 | var ioClient = new HttpClient(); 156 | ioClient 157 | .getUrl(authCodeCall) 158 | .then((request) => request.close()) 159 | .then((response) => response.drain()) 160 | .whenComplete(expectAsync0(() { 161 | ioClient.close(); 162 | })); 163 | } 164 | 165 | void userPrompt(String url) { 166 | var redirectUri = validateUserPromptUri(url); 167 | var authCodeCall = new Uri( 168 | scheme: redirectUri.scheme, 169 | host: redirectUri.host, 170 | port: redirectUri.port, 171 | path: redirectUri.path, 172 | queryParameters: { 173 | 'state': Uri.parse(url).queryParameters['state'], 174 | 'code': 'mycode', 175 | }); 176 | callRedirectionEndpoint(authCodeCall); 177 | } 178 | 179 | void userPromptInvalidAuthCodeCallback(String url) { 180 | var redirectUri = validateUserPromptUri(url); 181 | var authCodeCall = new Uri( 182 | scheme: redirectUri.scheme, 183 | host: redirectUri.host, 184 | port: redirectUri.port, 185 | path: redirectUri.path, 186 | queryParameters: { 187 | 'state': Uri.parse(url).queryParameters['state'], 188 | 'error': 'failed to authenticate', 189 | }); 190 | callRedirectionEndpoint(authCodeCall); 191 | } 192 | 193 | test('successfull', () async { 194 | var flow = new AuthorizationCodeGrantServerFlow( 195 | clientId, 196 | scopes, 197 | mockClient(successFullResponse(manual: false), expectClose: false), 198 | expectAsync1(userPrompt)); 199 | validateAccessCredentials(await flow.run()); 200 | }); 201 | 202 | test('transport-exception', () { 203 | var flow = new AuthorizationCodeGrantServerFlow( 204 | clientId, scopes, transportFailure, expectAsync1(userPrompt)); 205 | expect(flow.run(), throwsA(isTransportException)); 206 | }); 207 | 208 | test('invalid-server-response', () { 209 | var flow = new AuthorizationCodeGrantServerFlow( 210 | clientId, 211 | scopes, 212 | mockClient(invalidResponse, expectClose: false), 213 | expectAsync1(userPrompt)); 214 | expect(flow.run(), throwsA(isException)); 215 | }); 216 | 217 | test('failed-authentication', () { 218 | var flow = new AuthorizationCodeGrantServerFlow( 219 | clientId, 220 | scopes, 221 | mockClient(successFullResponse(manual: false), expectClose: false), 222 | expectAsync1(userPromptInvalidAuthCodeCallback)); 223 | expect(flow.run(), throwsA(isUserConsentException)); 224 | }); 225 | }, testOn: '!browser'); 226 | }); 227 | 228 | group('scopes-from-tokeninfo-endpoint', () { 229 | var successfulResponseJson = jsonEncode({ 230 | "issued_to": "XYZ.apps.googleusercontent.com", 231 | "audience": "XYZ.apps.googleusercontent.com", 232 | "scope": "scopeA scopeB", 233 | "expires_in": 3210, 234 | "access_type": "offline" 235 | }); 236 | var expectedUri = 237 | 'https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=my_token'; 238 | 239 | test('successfull', () async { 240 | var http = mockClient(expectAsync1((BaseRequest request) async { 241 | expect(request.url.toString(), expectedUri); 242 | return new Response(successfulResponseJson, 200); 243 | }), expectClose: false); 244 | List scopes = await obtainScopesFromAccessToken('my_token', http); 245 | expect(scopes, equals(['scopeA', 'scopeB'])); 246 | }); 247 | 248 | test('non-200-status-code', () { 249 | var http = mockClient(expectAsync1((BaseRequest request) async { 250 | expect(request.url.toString(), expectedUri); 251 | return new Response(successfulResponseJson, 201); 252 | }), expectClose: false); 253 | expect(obtainScopesFromAccessToken('my_token', http), throwsException); 254 | }); 255 | 256 | test('no-scope', () { 257 | var http = mockClient(expectAsync1((BaseRequest request) async { 258 | expect(request.url.toString(), expectedUri); 259 | return new Response(jsonEncode({}), 200); 260 | }), expectClose: false); 261 | expect(obtainScopesFromAccessToken('my_token', http), throwsException); 262 | }); 263 | }); 264 | } 265 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_hybrid_force.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() {} 27 | GapiAuth.prototype.init = function(doneCallback) { 28 | doneCallback(); 29 | }; 30 | GapiAuth.prototype.authorize = function(json, doneCallback) { 31 | /* 32 | Input: 33 | argument1 = { 34 | 'client_id' 35 | 'immediate' 36 | 'approval_prompt' 37 | 'response_type' 38 | 'scope' 39 | 'access_type' 40 | }; 41 | argument2 = dartCallback(json); 42 | 43 | Output: 44 | output_1 = { 45 | 'token_type', 46 | 'access_token', 47 | 'expires_in', 48 | 'code', 49 | 'state', 50 | 'error', 51 | }; 52 | */ 53 | 54 | var client_id = json['client_id']; 55 | var immediate = json['immediate']; 56 | var approval_prompt = json['approval_prompt']; 57 | var response_type = json['response_type']; 58 | var scope = json['scope']; 59 | var access_type = json['access_type']; 60 | 61 | if (client_id == 'foo_client' && 62 | immediate == false && 63 | approval_prompt == 'force' && 64 | response_type == 'code token' && 65 | scope == 'scope1 scope2' && 66 | access_type == 'offline') { 67 | doneCallback({ 68 | 'token_type' : 'Bearer', 69 | 'access_token' : 'foo_token', 70 | 'expires_in' : '3210', 71 | 'code' : 'mycode' 72 | }); 73 | } else { 74 | throw 'error'; 75 | } 76 | }; 77 | 78 | // Initialize the gapi.auth mock. 79 | window.gapi = new Object(); 80 | window.gapi.auth = new GapiAuth(); 81 | 82 | // Call the dart function. This signals that gapi.auth was loaded. 83 | var dartFunction = findDartOnLoadCallback(); 84 | dartFunction(); 85 | })(); 86 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_hybrid_force_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | import 'package:googleapis_auth/src/utils.dart' as utils; 10 | 11 | import 'utils.dart'; 12 | 13 | main() { 14 | impl.gapiUrl = resource('gapi_auth_hybrid_force.js'); 15 | 16 | test('gapi-auth-hybrid-force-test', () async { 17 | var clientId = new auth.ClientId('foo_client', 'foo_secret'); 18 | var scopes = ['scope1', 'scope2']; 19 | 20 | auth.BrowserOAuth2Flow flow = 21 | await auth.createImplicitBrowserFlow(clientId, scopes); 22 | auth.HybridFlowResult result = await flow.runHybridFlow(); 23 | 24 | var credentials = result.credentials; 25 | 26 | var date = new DateTime.now().toUtc().add( 27 | const Duration(seconds: 3210 - utils.MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 28 | var difference = credentials.accessToken.expiry.difference(date); 29 | var seconds = difference.inSeconds; 30 | 31 | expect(-3 <= seconds && seconds <= 3, isTrue); 32 | expect(credentials.accessToken.data, 'foo_token'); 33 | expect(credentials.refreshToken, isNull); 34 | expect(credentials.scopes, hasLength(2)); 35 | expect(credentials.scopes[0], 'scope1'); 36 | expect(credentials.scopes[1], 'scope2'); 37 | 38 | expect(result.authorizationCode, 'mycode'); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_hybrid_immediate.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() {} 27 | GapiAuth.prototype.init = function(doneCallback) { 28 | doneCallback(); 29 | }; 30 | GapiAuth.prototype.authorize = function(json, doneCallback) { 31 | /* 32 | Input: 33 | argument1 = { 34 | 'client_id' 35 | 'immediate' 36 | 'approval_prompt' 37 | 'response_type' 38 | 'scope' 39 | 'access_type' 40 | }; 41 | argument2 = dartCallback(json); 42 | 43 | Output: 44 | output_1 = { 45 | 'token_type', 46 | 'access_token', 47 | 'expires_in', 48 | 'code', 49 | 'state', 50 | 'error', 51 | }; 52 | */ 53 | 54 | var client_id = json['client_id']; 55 | var immediate = json['immediate']; 56 | var approval_prompt = json['approval_prompt']; 57 | var response_type = json['response_type']; 58 | var scope = json['scope']; 59 | var access_type = json['access_type']; 60 | 61 | if (client_id == 'foo_client' && 62 | immediate == true && 63 | approval_prompt == 'auto' && 64 | response_type == 'code token' && 65 | scope == 'scope1 scope2' && 66 | access_type == 'offline') { 67 | doneCallback({ 68 | 'token_type' : 'Bearer', 69 | 'access_token' : 'foo_token', 70 | 'expires_in' : '3210', 71 | 'code' : 'mycode' 72 | }); 73 | } else { 74 | throw 'error'; 75 | } 76 | }; 77 | 78 | // Initialize the gapi.auth mock. 79 | window.gapi = new Object(); 80 | window.gapi.auth = new GapiAuth(); 81 | 82 | // Call the dart function. This signals that gapi.auth was loaded. 83 | var dartFunction = findDartOnLoadCallback(); 84 | dartFunction(); 85 | })(); 86 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_hybrid_immediate_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | import 'package:googleapis_auth/src/utils.dart' as utils; 10 | 11 | import 'utils.dart'; 12 | 13 | main() { 14 | impl.gapiUrl = resource('gapi_auth_hybrid_immediate.js'); 15 | 16 | test('gapi-auth-hybrid-immediate-test', () async { 17 | var clientId = new auth.ClientId('foo_client', 'foo_secret'); 18 | var scopes = ['scope1', 'scope2']; 19 | 20 | auth.BrowserOAuth2Flow flow = 21 | await auth.createImplicitBrowserFlow(clientId, scopes); 22 | auth.HybridFlowResult result = 23 | await flow.runHybridFlow(force: false, immediate: true); 24 | var credentials = result.credentials; 25 | 26 | var date = new DateTime.now().toUtc().add( 27 | const Duration(seconds: 3210 - utils.MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 28 | var difference = credentials.accessToken.expiry.difference(date); 29 | var seconds = difference.inSeconds; 30 | 31 | expect(-3 <= seconds && seconds <= 3, isTrue); 32 | expect(credentials.accessToken.data, 'foo_token'); 33 | expect(credentials.refreshToken, isNull); 34 | expect(credentials.scopes, hasLength(2)); 35 | expect(credentials.scopes[0], 'scope1'); 36 | expect(credentials.scopes[1], 'scope2'); 37 | 38 | expect(result.authorizationCode, 'mycode'); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() {} 27 | GapiAuth.prototype.init = function(doneCallback) { 28 | doneCallback(); 29 | }; 30 | GapiAuth.prototype.authorize = function(json, doneCallback) { 31 | /* 32 | Input: 33 | argument1 = { 34 | 'client_id' 35 | 'immediate' 36 | 'approval_prompt' 37 | 'response_type' 38 | 'scope' 39 | 'access_type' 40 | }; 41 | argument2 = dartCallback(json); 42 | 43 | Output: 44 | output_1 = { 45 | 'token_type', 46 | 'access_token', 47 | 'expires_in', 48 | 'code', 49 | 'state', 50 | 'error', 51 | }; 52 | */ 53 | 54 | var client_id = json['client_id']; 55 | var immediate = json['immediate']; 56 | var approval_prompt = json['approval_prompt']; 57 | var response_type = json['response_type']; 58 | var scope = json['scope']; 59 | var access_type = json['access_type']; 60 | 61 | if (client_id == 'foo_client' && 62 | immediate == false && 63 | approval_prompt == 'auto' && 64 | response_type == 'code token' && 65 | scope == 'scope1 scope2' && 66 | access_type == 'offline') { 67 | doneCallback({ 68 | 'token_type' : 'Bearer', 69 | 'access_token' : 'foo_token', 70 | 'expires_in' : '3210', 71 | 'code' : 'mycode' 72 | }); 73 | } else { 74 | throw 'error'; 75 | } 76 | }; 77 | 78 | // Initialize the gapi.auth mock. 79 | window.gapi = new Object(); 80 | window.gapi.auth = new GapiAuth(); 81 | 82 | // Call the dart function. This signals that gapi.auth was loaded. 83 | var dartFunction = findDartOnLoadCallback(); 84 | dartFunction(); 85 | })(); 86 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | import 'package:googleapis_auth/src/utils.dart' as utils; 10 | 11 | import 'utils.dart'; 12 | 13 | main() { 14 | impl.gapiUrl = resource('gapi_auth_hybrid_nonforce.js'); 15 | 16 | test('gapi-auth-hybrid-nonforce-test', () async { 17 | var clientId = new auth.ClientId('foo_client', 'foo_secret'); 18 | var scopes = ['scope1', 'scope2']; 19 | 20 | auth.BrowserOAuth2Flow flow = 21 | await auth.createImplicitBrowserFlow(clientId, scopes); 22 | auth.HybridFlowResult result = await flow.runHybridFlow(force: false); 23 | var credentials = result.credentials; 24 | 25 | var date = new DateTime.now().toUtc().add( 26 | const Duration(seconds: 3210 - utils.MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 27 | var difference = credentials.accessToken.expiry.difference(date); 28 | var seconds = difference.inSeconds; 29 | 30 | expect(-3 <= seconds && seconds <= 3, isTrue); 31 | expect(credentials.accessToken.data, 'foo_token'); 32 | expect(credentials.refreshToken, isNull); 33 | expect(credentials.scopes, hasLength(2)); 34 | expect(credentials.scopes[0], 'scope1'); 35 | expect(credentials.scopes[1], 'scope2'); 36 | 37 | expect(result.authorizationCode, 'mycode'); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_immediate.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() {} 27 | GapiAuth.prototype.init = function(doneCallback) { 28 | doneCallback(); 29 | }; 30 | GapiAuth.prototype.authorize = function(json, doneCallback) { 31 | /* 32 | Input: 33 | argument1 = { 34 | 'client_id' 35 | 'immediate' 36 | 'approval_prompt' 37 | 'response_type' 38 | 'scope' 39 | 'access_type' 40 | }; 41 | argument2 = dartCallback(json); 42 | 43 | Output: 44 | output_1 = { 45 | 'token_type', 46 | 'access_token', 47 | 'expires_in', 48 | 'code', 49 | 'state', 50 | 'error', 51 | }; 52 | */ 53 | 54 | var client_id = json['client_id']; 55 | var immediate = json['immediate']; 56 | var approval_prompt = json['approval_prompt']; 57 | var response_type = json['response_type']; 58 | var scope = json['scope']; 59 | var access_type = json['access_type']; 60 | 61 | if (client_id == 'foo_client' && 62 | immediate == true && 63 | approval_prompt == 'auto' && 64 | response_type == 'token' && 65 | scope == 'scope1 scope2' && 66 | access_type == 'online') { 67 | doneCallback({ 68 | 'token_type' : 'Bearer', 69 | 'access_token' : 'foo_token', 70 | 'expires_in' : '3210' 71 | }); 72 | } else { 73 | throw 'error'; 74 | } 75 | }; 76 | 77 | // Initialize the gapi.auth mock. 78 | window.gapi = new Object(); 79 | window.gapi.auth = new GapiAuth(); 80 | 81 | // Call the dart function. This signals that gapi.auth was loaded. 82 | var dartFunction = findDartOnLoadCallback(); 83 | dartFunction(); 84 | })(); 85 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_immediate_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | import 'package:googleapis_auth/src/utils.dart' as utils; 10 | 11 | import 'utils.dart'; 12 | 13 | main() { 14 | impl.gapiUrl = resource('gapi_auth_immediate.js'); 15 | 16 | test('gapi-auth-force', () async { 17 | var clientId = new auth.ClientId('foo_client', 'foo_secret'); 18 | var scopes = ['scope1', 'scope2']; 19 | 20 | auth.BrowserOAuth2Flow flow = 21 | await auth.createImplicitBrowserFlow(clientId, scopes); 22 | auth.AccessCredentials credentials = 23 | await flow.obtainAccessCredentialsViaUserConsent(immediate: true); 24 | var date = new DateTime.now().toUtc().add( 25 | const Duration(seconds: 3210 - utils.MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 26 | var difference = credentials.accessToken.expiry.difference(date); 27 | var seconds = difference.inSeconds; 28 | 29 | expect(-3 <= seconds && seconds <= 3, isTrue); 30 | expect(credentials.accessToken.data, 'foo_token'); 31 | expect(credentials.refreshToken, isNull); 32 | expect(credentials.scopes, hasLength(2)); 33 | expect(credentials.scopes[0], 'scope1'); 34 | expect(credentials.scopes[1], 'scope2'); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_implicit_idtoken.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function () { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() { } 27 | GapiAuth.prototype.init = function (doneCallback) { 28 | doneCallback(); 29 | }; 30 | GapiAuth.prototype.authorize = function (json, doneCallback) { 31 | /* 32 | Input: 33 | argument1 = { 34 | 'client_id' 35 | 'immediate' 36 | 'approval_prompt' 37 | 'response_type' 38 | 'scope' 39 | 'access_type' 40 | }; 41 | argument2 = dartCallback(json); 42 | 43 | Output: 44 | output_1 = { 45 | 'token_type', 46 | 'access_token', 47 | 'expires_in', 48 | 'id_token', 49 | 'code', 50 | 'state', 51 | 'error', 52 | }; 53 | */ 54 | 55 | var client_id = json['client_id']; 56 | var immediate = json['immediate']; 57 | var approval_prompt = json['approval_prompt']; 58 | var response_type = json['response_type']; 59 | var scope = json['scope']; 60 | var access_type = json['access_type']; 61 | 62 | if (client_id == 'foo_client' && 63 | immediate == false && 64 | approval_prompt == 'auto' && 65 | response_type == 'id_token token' && 66 | scope == 'scope1 scope2' && 67 | access_type == 'online') { 68 | doneCallback({ 69 | 'token_type': 'Bearer', 70 | 'access_token': 'foo_token', 71 | 'id_token': 'foo_id_token', 72 | 'expires_in': '3210' 73 | }); 74 | } else { 75 | throw 'error'; 76 | } 77 | }; 78 | 79 | // Initialize the gapi.auth mock. 80 | window.gapi = new Object(); 81 | window.gapi.auth = new GapiAuth(); 82 | 83 | // Call the dart function. This signals that gapi.auth was loaded. 84 | var dartFunction = findDartOnLoadCallback(); 85 | dartFunction(); 86 | })(); 87 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_implicit_idtoken_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | import 'package:googleapis_auth/src/utils.dart' as utils; 10 | 11 | import 'utils.dart'; 12 | 13 | main() { 14 | impl.gapiUrl = resource('gapi_auth_implicit_idtoken.js'); 15 | 16 | test('gapi-auth-implicit-idtoken', () async { 17 | var clientId = new auth.ClientId('foo_client', 'foo_secret'); 18 | var scopes = ['scope1', 'scope2']; 19 | 20 | auth.BrowserOAuth2Flow flow = 21 | await auth.createImplicitBrowserFlow(clientId, scopes); 22 | auth.AccessCredentials credentials = await flow 23 | .obtainAccessCredentialsViaUserConsent(responseTypes: [ 24 | auth.ResponseType.idToken, 25 | auth.ResponseType.token 26 | ]); 27 | 28 | var date = new DateTime.now().toUtc().add( 29 | const Duration(seconds: 3210 - utils.MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 30 | var difference = credentials.accessToken.expiry.difference(date); 31 | var seconds = difference.inSeconds; 32 | 33 | expect(-3 <= seconds && seconds <= 3, isTrue); 34 | expect(credentials.accessToken.data, 'foo_token'); 35 | expect(credentials.refreshToken, isNull); 36 | expect(credentials.scopes, hasLength(2)); 37 | expect(credentials.scopes[0], 'scope1'); 38 | expect(credentials.scopes[1], 'scope2'); 39 | expect(credentials.idToken, 'foo_id_token'); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_nonforce.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() {} 27 | GapiAuth.prototype.init = function(doneCallback) { 28 | doneCallback(); 29 | }; 30 | GapiAuth.prototype.authorize = function(json, doneCallback) { 31 | /* 32 | Input: 33 | argument1 = { 34 | 'client_id' 35 | 'immediate' 36 | 'approval_prompt' 37 | 'response_type' 38 | 'scope' 39 | 'access_type' 40 | }; 41 | argument2 = dartCallback(json); 42 | 43 | Output: 44 | output_1 = { 45 | 'token_type', 46 | 'access_token', 47 | 'expires_in', 48 | 'code', 49 | 'state', 50 | 'error', 51 | }; 52 | */ 53 | 54 | var client_id = json['client_id']; 55 | var immediate = json['immediate']; 56 | var approval_prompt = json['approval_prompt']; 57 | var response_type = json['response_type']; 58 | var scope = json['scope']; 59 | var access_type = json['access_type']; 60 | 61 | if (client_id == 'foo_client' && 62 | immediate == false && 63 | approval_prompt == 'auto' && 64 | response_type == 'token' && 65 | scope == 'scope1 scope2' && 66 | access_type == 'online') { 67 | doneCallback({ 68 | 'token_type' : 'Bearer', 69 | 'access_token' : 'foo_token', 70 | 'expires_in' : '3210' 71 | }); 72 | } else { 73 | throw 'error'; 74 | } 75 | }; 76 | 77 | // Initialize the gapi.auth mock. 78 | window.gapi = new Object(); 79 | window.gapi.auth = new GapiAuth(); 80 | 81 | // Call the dart function. This signals that gapi.auth was loaded. 82 | var dartFunction = findDartOnLoadCallback(); 83 | dartFunction(); 84 | })(); 85 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_nonforce_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | import 'package:googleapis_auth/src/utils.dart' as utils; 10 | 11 | import 'utils.dart'; 12 | 13 | main() { 14 | impl.gapiUrl = resource('gapi_auth_nonforce.js'); 15 | 16 | test('gapi-auth-nonforce', () async { 17 | var clientId = new auth.ClientId('foo_client', 'foo_secret'); 18 | var scopes = ['scope1', 'scope2']; 19 | 20 | auth.BrowserOAuth2Flow flow = 21 | await auth.createImplicitBrowserFlow(clientId, scopes); 22 | auth.AccessCredentials credentials = 23 | await flow.obtainAccessCredentialsViaUserConsent(); 24 | 25 | var date = new DateTime.now().toUtc().add( 26 | const Duration(seconds: 3210 - utils.MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 27 | var difference = credentials.accessToken.expiry.difference(date); 28 | var seconds = difference.inSeconds; 29 | 30 | expect(-3 <= seconds && seconds <= 3, isTrue); 31 | expect(credentials.accessToken.data, 'foo_token'); 32 | expect(credentials.refreshToken, isNull); 33 | expect(credentials.scopes, hasLength(2)); 34 | expect(credentials.scopes[0], 'scope1'); 35 | expect(credentials.scopes[1], 'scope2'); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_user_denied.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() {} 27 | GapiAuth.prototype.init = function(doneCallback) { 28 | doneCallback(); 29 | }; 30 | GapiAuth.prototype.authorize = function(json, doneCallback) { 31 | /* 32 | Input: 33 | argument1 = { 34 | 'client_id' 35 | 'immediate' 36 | 'approval_prompt' 37 | 'response_type' 38 | 'scope' 39 | 'access_type' 40 | }; 41 | argument2 = dartCallback(json); 42 | 43 | Output: 44 | output_1 = { 45 | 'token_type', 46 | 'access_token', 47 | 'expires_in', 48 | 'code', 49 | 'state', 50 | 'error', 51 | }; 52 | */ 53 | 54 | var client_id = json['client_id']; 55 | var immediate = json['immediate']; 56 | var approval_prompt = json['approval_prompt']; 57 | var response_type = json['response_type']; 58 | var scope = json['scope']; 59 | var access_type = json['access_type']; 60 | 61 | if (client_id == 'foo_client' && 62 | immediate == false && 63 | approval_prompt == 'auto' && 64 | response_type == 'token' && 65 | scope == 'scope1 scope2' && 66 | access_type == 'online') { 67 | doneCallback({ 68 | 'error' : 'failed to get user consent', 69 | }); 70 | } else { 71 | throw 'error'; 72 | } 73 | }; 74 | 75 | // Initialize the gapi.auth mock. 76 | window.gapi = new Object(); 77 | window.gapi.auth = new GapiAuth(); 78 | 79 | // Call the dart function. This signals that gapi.auth was loaded. 80 | var dartFunction = findDartOnLoadCallback(); 81 | dartFunction(); 82 | })(); -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_auth_user_denied_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | 10 | import 'utils.dart'; 11 | 12 | main() { 13 | impl.gapiUrl = resource('gapi_auth_user_denied.js'); 14 | 15 | test('gapi-auth-user-denied', () async { 16 | var clientId = new auth.ClientId('foo_client', 'foo_secret'); 17 | var scopes = ['scope1', 'scope2']; 18 | 19 | auth.BrowserOAuth2Flow flow = 20 | await auth.createImplicitBrowserFlow(clientId, scopes); 21 | try { 22 | await flow.obtainAccessCredentialsViaUserConsent(); 23 | fail('expected error'); 24 | } catch (error) { 25 | expect(error is auth.UserConsentException, isTrue); 26 | } 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_initialize_failure.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | function GapiAuth() {} 27 | // We do not set the init/authorize functions, which should make the 28 | // the initialization fail. 29 | // GapiAuth.prototype.init = ...; 30 | // GapiAuth.prototype.authorize = ...; 31 | 32 | // Initialize the gapi.auth mock. 33 | window.gapi = new Object(); 34 | window.gapi.auth = new GapiAuth(); 35 | 36 | // Call the dart function. This signals that gapi.auth was loaded. 37 | var dartFunction = findDartOnLoadCallback(); 38 | dartFunction(); 39 | })(); -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_initialize_failure_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | 10 | import 'utils.dart'; 11 | 12 | main() { 13 | impl.gapiUrl = resource('gapi_initialize_failure.js'); 14 | 15 | test('gapi-initialize-failure', () { 16 | var clientId = new auth.ClientId('a', 'b'); 17 | var scopes = ['scope1', 'scope2']; 18 | 19 | expect(auth.createImplicitBrowserFlow(clientId, scopes), throwsStateError); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_initialize_successful.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | (function() { 6 | // This function looks up the URL this script was loaded in and finds the 7 | // name of the callback function to call when the library is read. 8 | // The URL of the script load looks like: 9 | // http://localhost:8080/folder/file?onload=dartGapiLoaded 10 | function findDartOnLoadCallback() { 11 | var scripts = document.getElementsByTagName('script'); 12 | var self = scripts[scripts.length - 1]; 13 | 14 | var equalsSign = self.src.indexOf('='); 15 | if (equalsSign <= 0) throw 'error'; 16 | 17 | var callbackName = self.src.substring(equalsSign + 1); 18 | if (callbackName.length <= 0) throw 'error'; 19 | 20 | var dartFunction = window[callbackName]; 21 | if (dartFunction == null) throw 'error'; 22 | 23 | return dartFunction; 24 | } 25 | 26 | // Initialize the gapi.auth mock. 27 | function GapiAuth() {} 28 | GapiAuth.prototype.init = function (dartCallback) { 29 | dartCallback(); 30 | }; 31 | window.gapi = new Object(); 32 | window.gapi.auth = new GapiAuth(); 33 | 34 | // Call the dart function. This signals that gapi.auth was loaded. 35 | var dartFunction = findDartOnLoadCallback(); 36 | dartFunction(); 37 | })(); -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_initialize_successful_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'package:test/test.dart'; 7 | import 'package:googleapis_auth/auth_browser.dart' as auth; 8 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 9 | 10 | import 'utils.dart'; 11 | 12 | main() { 13 | impl.gapiUrl = resource('gapi_initialize_successful.js'); 14 | 15 | test('gapi-initialize-successful', () { 16 | var clientId = new auth.ClientId('a', 'b'); 17 | var clientId2 = new auth.ClientId('c', 'd'); 18 | var scopes = ['scope1', 'scope2']; 19 | 20 | expect(auth.createImplicitBrowserFlow(clientId, scopes), completes); 21 | expect(auth.createImplicitBrowserFlow(clientId2, scopes), completes); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_load_failure.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | // We do not set 'window.gapi = ...' 6 | this is a syntax error -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/gapi_load_failure_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn('browser') 6 | import 'dart:html'; 7 | import 'dart:js' as js; 8 | 9 | import 'package:test/test.dart'; 10 | import 'package:googleapis_auth/auth_browser.dart' as auth; 11 | import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl; 12 | 13 | import 'utils.dart'; 14 | 15 | main() { 16 | // The default timeout is too small for us to detect the timeout of loading 17 | // the gapi.auth library. 18 | Timeout timeout = const Timeout(const Duration(hours: 1)); 19 | 20 | var clientId = new auth.ClientId('a', 'b'); 21 | var scopes = ['scope1', 'scope2']; 22 | 23 | test('gapi-load-failure', () { 24 | impl.gapiUrl = resource('non_existent.js'); 25 | expect(auth.createImplicitBrowserFlow(clientId, scopes), throwsException); 26 | }, timeout: timeout); 27 | 28 | test('gapi-load-failure--syntax-error', () async { 29 | impl.gapiUrl = resource('gapi_load_failure.js'); 30 | 31 | // Reset test_controller.js's window.onerror registration. 32 | // This makes sure we can catch the onError callback when the syntax error 33 | // is produced. 34 | js.context['onerror'] = null; 35 | 36 | window.onError.listen(expectAsync1((error) { 37 | error.preventDefault(); 38 | })); 39 | 40 | var sw = new Stopwatch()..start(); 41 | try { 42 | await auth.createImplicitBrowserFlow(clientId, scopes); 43 | fail('expected error'); 44 | } catch (error) { 45 | var elapsed = (sw.elapsed - impl.ImplicitFlow.CallbackTimeout).inSeconds; 46 | expect(-3 <= elapsed && elapsed <= 3, isTrue); 47 | } 48 | }, timeout: timeout); 49 | } 50 | -------------------------------------------------------------------------------- /test/oauth2_flows/implicit/utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | import 'dart:html'; 6 | 7 | String resource(String name) => 8 | Uri.parse(document.baseUri!).resolve(name).toString(); 9 | -------------------------------------------------------------------------------- /test/oauth2_flows/jwt_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.jwt_test; 6 | 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | 10 | import 'package:googleapis_auth/src/oauth2_flows/jwt.dart'; 11 | import 'package:http/http.dart'; 12 | import 'package:test/test.dart'; 13 | 14 | import '../test_utils.dart'; 15 | 16 | main() { 17 | var tokenUrl = 'https://accounts.google.com/o/oauth2/token'; 18 | 19 | Future successfulSignRequest(Request request) { 20 | expect(request.method, equals('POST')); 21 | expect(request.url.toString(), equals(tokenUrl)); 22 | 23 | // We are not asserting what comes after '&assertion=' because this is 24 | // time dependent. 25 | expect( 26 | request.body, 27 | startsWith( 28 | 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer' 29 | '&assertion=')); 30 | var body = jsonEncode({ 31 | 'access_token': 'atok', 32 | 'expires_in': 3600, 33 | 'token_type': 'Bearer', 34 | }); 35 | return new Future.value(new Response(body, 200)); 36 | } 37 | 38 | Future invalidAccessToken(Request request) { 39 | var body = jsonEncode({ 40 | // Missing 'expires_in' entry 41 | 'access_token': 'atok', 42 | 'token_type': 'Bearer', 43 | }); 44 | return new Future.value(new Response(body, 200)); 45 | } 46 | 47 | group('jwt-flow', () { 48 | var clientEmail = 'a@b.com'; 49 | var scopes = ['s1', 's2']; 50 | 51 | test('successfull', () async { 52 | var flow = new JwtFlow(clientEmail, testPrivateKey, null, scopes, 53 | mockClient(expectAsync1(successfulSignRequest), expectClose: false)); 54 | 55 | var credentials = await flow.run(); 56 | expect(credentials.accessToken.data, equals('atok')); 57 | expect(credentials.accessToken.type, equals('Bearer')); 58 | expect(credentials.scopes, equals(['s1', 's2'])); 59 | expectExpiryOneHourFromNow(credentials.accessToken); 60 | }); 61 | 62 | test('successfull-with-user', () async { 63 | var flow = new JwtFlow(clientEmail, testPrivateKey, 'x@y.com', scopes, 64 | mockClient(expectAsync1(successfulSignRequest), expectClose: false)); 65 | 66 | var credentials = await flow.run(); 67 | expect(credentials.accessToken.data, equals('atok')); 68 | expect(credentials.accessToken.type, equals('Bearer')); 69 | expect(credentials.scopes, equals(['s1', 's2'])); 70 | expectExpiryOneHourFromNow(credentials.accessToken); 71 | }); 72 | 73 | test('invalid-server-response', () { 74 | var flow = new JwtFlow(clientEmail, testPrivateKey, null, scopes, 75 | mockClient(expectAsync1(invalidAccessToken), expectClose: false)); 76 | 77 | expect(flow.run(), throwsA(isException)); 78 | }); 79 | 80 | test('transport-failure', () { 81 | var flow = new JwtFlow( 82 | clientEmail, testPrivateKey, null, scopes, transportFailure); 83 | 84 | expect(flow.run(), throwsA(isTransportException)); 85 | }); 86 | }); 87 | } 88 | -------------------------------------------------------------------------------- /test/oauth2_flows/metadata_server_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 | @TestOn("vm") 6 | library googleapis_auth.metadata_server; 7 | 8 | import 'dart:async'; 9 | import 'dart:convert'; 10 | 11 | import 'package:googleapis_auth/src/oauth2_flows/metadata_server.dart'; 12 | import 'package:http/http.dart'; 13 | import 'package:test/test.dart'; 14 | 15 | import '../test_utils.dart'; 16 | 17 | main() { 18 | var apiUrl = 'http://metadata/computeMetadata/v1'; 19 | var apiHeaderKey = 'Metadata-Flavor'; 20 | var apiHeaderValue = 'Google'; 21 | var tokenUrl = '$apiUrl/instance/service-accounts/default/token'; 22 | var scopesUrl = '$apiUrl/instance/service-accounts/default/scopes'; 23 | 24 | Future successfulAccessToken(Request request) { 25 | expect(request.method, equals('GET')); 26 | expect(request.url.toString(), equals(tokenUrl)); 27 | expect(request.headers[apiHeaderKey], equals(apiHeaderValue)); 28 | 29 | var body = jsonEncode({ 30 | 'access_token': 'atok', 31 | 'expires_in': 3600, 32 | 'token_type': 'Bearer', 33 | }); 34 | return new Future.value(new Response(body, 200)); 35 | } 36 | 37 | Future invalidAccessToken(Request request) { 38 | expect(request.method, equals('GET')); 39 | expect(request.url.toString(), equals(tokenUrl)); 40 | expect(request.headers[apiHeaderKey], equals(apiHeaderValue)); 41 | 42 | var body = jsonEncode({ 43 | // Missing 'expires_in' entry 44 | 'access_token': 'atok', 45 | 'token_type': 'Bearer', 46 | }); 47 | return new Future.value(new Response(body, 200)); 48 | } 49 | 50 | Future successfulScopes(Request request) { 51 | expect(request.method, equals('GET')); 52 | expect(request.url.toString(), equals(scopesUrl)); 53 | expect(request.headers[apiHeaderKey], equals(apiHeaderValue)); 54 | 55 | return new Future.value(new Response('s1\ns2', 200)); 56 | } 57 | 58 | group('metadata-server-authorization-flow', () { 59 | test('successfull', () async { 60 | var flow = new MetadataServerAuthorizationFlow(mockClient( 61 | expectAsync1((request) { 62 | var url = request.url.toString(); 63 | if (url == tokenUrl) { 64 | return successfulAccessToken(request); 65 | } else if (url == scopesUrl) { 66 | return successfulScopes(request); 67 | } else { 68 | fail("Invalid URL $url (expected: $tokenUrl or $scopesUrl)."); 69 | } 70 | }, count: 2), 71 | expectClose: false)); 72 | 73 | var credentials = await flow.run(); 74 | expect(credentials.accessToken.data, equals('atok')); 75 | expect(credentials.accessToken.type, equals('Bearer')); 76 | expect(credentials.scopes, equals(['s1', 's2'])); 77 | expectExpiryOneHourFromNow(credentials.accessToken); 78 | }); 79 | 80 | test('invalid-server-reponse', () { 81 | int requestNr = 0; 82 | var flow = new MetadataServerAuthorizationFlow(mockClient( 83 | expectAsync1((request) { 84 | if (requestNr++ == 0) 85 | return invalidAccessToken(request); 86 | else 87 | return successfulScopes(request); 88 | }, count: 2), 89 | expectClose: false)); 90 | expect(flow.run(), throwsA(isException)); 91 | }); 92 | 93 | test('token-transport-error', () { 94 | int requestNr = 0; 95 | var flow = new MetadataServerAuthorizationFlow(mockClient( 96 | expectAsync1((request) { 97 | if (requestNr++ == 0) 98 | return transportFailure.get(Uri.http('failure', '')); 99 | else 100 | return successfulScopes(request); 101 | }, count: 2), 102 | expectClose: false)); 103 | expect(flow.run(), throwsA(isTransportException)); 104 | }); 105 | 106 | test('scopes-transport-error', () { 107 | int requestNr = 0; 108 | var flow = new MetadataServerAuthorizationFlow(mockClient( 109 | expectAsync1((request) { 110 | if (requestNr++ == 0) 111 | return successfulAccessToken(request); 112 | else 113 | return transportFailure.get(Uri.http('failure', '')); 114 | }, count: 2), 115 | expectClose: false)); 116 | expect(flow.run(), throwsA(isTransportException)); 117 | }); 118 | }); 119 | } 120 | -------------------------------------------------------------------------------- /test/oauth2_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.oauth2_test; 6 | 7 | import 'dart:async'; 8 | import 'dart:convert'; 9 | 10 | import 'package:googleapis_auth/auth.dart'; 11 | import 'package:googleapis_auth/src/utils.dart'; 12 | import 'package:googleapis_auth/src/http_client_base.dart'; 13 | import 'package:test/test.dart'; 14 | import 'package:http/http.dart'; 15 | 16 | import 'test_utils.dart'; 17 | 18 | final _defaultResponse = Response('', 500); 19 | final _defaultResponseHandler = (Request _) async => _defaultResponse; 20 | 21 | main() { 22 | test('access-token', () { 23 | var expiry = new DateTime.now().subtract(const Duration(seconds: 1)); 24 | var expiryUtc = expiry.toUtc(); 25 | 26 | expect(() => new AccessToken('foo', 'bar', expiry), throwsArgumentError); 27 | 28 | var token = new AccessToken('foo', 'bar', expiryUtc); 29 | expect(token.type, equals('foo')); 30 | expect(token.data, equals('bar')); 31 | expect(token.expiry, equals(expiryUtc)); 32 | expect(token.hasExpired, isTrue); 33 | 34 | var nonExpiredToken = 35 | new AccessToken('foo', 'bar', expiryUtc.add(const Duration(days: 1))); 36 | expect(nonExpiredToken.hasExpired, isFalse); 37 | }); 38 | 39 | test('access-credentials', () { 40 | var expiry = new DateTime.now().add(const Duration(days: 1)).toUtc(); 41 | var aToken = new AccessToken('foo', 'bar', expiry); 42 | 43 | var credentials = new AccessCredentials(aToken, 'refresh', ['scope']); 44 | expect(credentials.accessToken, equals(aToken)); 45 | expect(credentials.refreshToken, equals('refresh')); 46 | expect(credentials.scopes, equals(['scope'])); 47 | }); 48 | 49 | test('client-id', () { 50 | var clientId = new ClientId('id', 'secret'); 51 | expect(clientId.identifier, equals('id')); 52 | expect(clientId.secret, equals('secret')); 53 | }); 54 | 55 | group('service-account-credentials', () { 56 | var clientId = new ClientId.serviceAccount('id'); 57 | 58 | var credentials = const { 59 | "private_key_id": "301029", 60 | "private_key": TestPrivateKeyString, 61 | "client_email": "a@b.com", 62 | "client_id": "myid", 63 | "type": "service_account" 64 | }; 65 | 66 | test('from-valid-individual-params', () { 67 | var credentials = new ServiceAccountCredentials( 68 | 'email', clientId, TestPrivateKeyString); 69 | expect(credentials.email, equals('email')); 70 | expect(credentials.clientId, equals(clientId)); 71 | expect(credentials.privateKey, equals(TestPrivateKeyString)); 72 | expect(credentials.impersonatedUser, isNull); 73 | }); 74 | 75 | test('from-valid-individual-params-with-user', () { 76 | var credentials = new ServiceAccountCredentials( 77 | 'email', clientId, TestPrivateKeyString, 78 | impersonatedUser: 'x@y.com'); 79 | expect(credentials.email, equals('email')); 80 | expect(credentials.clientId, equals(clientId)); 81 | expect(credentials.privateKey, equals(TestPrivateKeyString)); 82 | expect(credentials.impersonatedUser, equals('x@y.com')); 83 | }); 84 | 85 | test('from-json-string', () { 86 | var credentialsFromJson = 87 | new ServiceAccountCredentials.fromJson(jsonEncode(credentials)); 88 | expect(credentialsFromJson.email, equals('a@b.com')); 89 | expect(credentialsFromJson.clientId.identifier, equals('myid')); 90 | expect(credentialsFromJson.clientId.secret, isNull); 91 | expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); 92 | expect(credentialsFromJson.impersonatedUser, isNull); 93 | }); 94 | 95 | test('from-json-string-with-user', () { 96 | var credentialsFromJson = new ServiceAccountCredentials.fromJson( 97 | jsonEncode(credentials), 98 | impersonatedUser: 'x@y.com'); 99 | expect(credentialsFromJson.email, equals('a@b.com')); 100 | expect(credentialsFromJson.clientId.identifier, equals('myid')); 101 | expect(credentialsFromJson.clientId.secret, isNull); 102 | expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); 103 | expect(credentialsFromJson.impersonatedUser, equals('x@y.com')); 104 | }); 105 | 106 | test('from-json-map', () { 107 | var credentialsFromJson = 108 | new ServiceAccountCredentials.fromJson(credentials); 109 | expect(credentialsFromJson.email, equals('a@b.com')); 110 | expect(credentialsFromJson.clientId.identifier, equals('myid')); 111 | expect(credentialsFromJson.clientId.secret, isNull); 112 | expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); 113 | expect(credentialsFromJson.impersonatedUser, isNull); 114 | }); 115 | 116 | test('from-json-map-with-user', () { 117 | var credentialsFromJson = new ServiceAccountCredentials.fromJson( 118 | credentials, 119 | impersonatedUser: 'x@y.com'); 120 | expect(credentialsFromJson.email, equals('a@b.com')); 121 | expect(credentialsFromJson.clientId.identifier, equals('myid')); 122 | expect(credentialsFromJson.clientId.secret, isNull); 123 | expect(credentialsFromJson.privateKey, equals(TestPrivateKeyString)); 124 | expect(credentialsFromJson.impersonatedUser, equals('x@y.com')); 125 | }); 126 | }); 127 | 128 | group('client-wrappers', () { 129 | var clientId = new ClientId('id', 'secret'); 130 | var tomorrow = new DateTime.now().add(const Duration(days: 1)).toUtc(); 131 | var yesterday = 132 | new DateTime.now().subtract(const Duration(days: 1)).toUtc(); 133 | var aToken = new AccessToken('Bearer', 'bar', tomorrow); 134 | var credentials = new AccessCredentials(aToken, 'refresh', ['s1', 's2']); 135 | 136 | Future successfulRefresh(Request request) { 137 | expect(request.method, equals('POST')); 138 | expect('${request.url}', 139 | equals('https://accounts.google.com/o/oauth2/token')); 140 | expect( 141 | request.body, 142 | equals('client_id=id&' 143 | 'client_secret=secret&' 144 | 'refresh_token=refresh&' 145 | 'grant_type=refresh_token')); 146 | var body = jsonEncode({ 147 | 'token_type': 'Bearer', 148 | 'access_token': 'atoken', 149 | 'expires_in': 3600, 150 | }); 151 | 152 | return new Future.value( 153 | new Response(body, 200, headers: _jsonContentType)); 154 | } 155 | 156 | Future refreshErrorResponse(Request request) { 157 | var body = jsonEncode({'error': 'An error occured'}); 158 | return new Future.value( 159 | new Response(body, 400, headers: _jsonContentType)); 160 | } 161 | 162 | Future serverError(Request request) { 163 | return new Future.error( 164 | new Exception('transport layer exception')); 165 | } 166 | 167 | test('refreshCredentials-successfull', () async { 168 | var newCredentials = await refreshCredentials(clientId, credentials, 169 | mockClient(expectAsync1(successfulRefresh), expectClose: false)); 170 | var expectedResultUtc = new DateTime.now().toUtc().add( 171 | const Duration(seconds: 3600 - MAX_EXPECTED_TIMEDIFF_IN_SECONDS)); 172 | 173 | var accessToken = newCredentials.accessToken; 174 | expect(accessToken.type, equals('Bearer')); 175 | expect(accessToken.data, equals('atoken')); 176 | expect(accessToken.expiry.difference(expectedResultUtc).inSeconds, 177 | equals(0)); 178 | 179 | expect(newCredentials.refreshToken, equals('refresh')); 180 | expect(newCredentials.scopes, equals(['s1', 's2'])); 181 | }); 182 | 183 | test('refreshCredentials-http-error', () async { 184 | try { 185 | await refreshCredentials( 186 | clientId, credentials, mockClient(serverError, expectClose: false)); 187 | fail('expected error'); 188 | } catch (error) { 189 | expect( 190 | error.toString(), equals('Exception: transport layer exception')); 191 | } 192 | }); 193 | 194 | test('refreshCredentials-error-response', () async { 195 | try { 196 | await refreshCredentials(clientId, credentials, 197 | mockClient(refreshErrorResponse, expectClose: false)); 198 | fail('expected error'); 199 | } catch (error) { 200 | expect(error is RefreshFailedException, isTrue); 201 | } 202 | }); 203 | 204 | group('authenticatedClient', () { 205 | var url = Uri.parse('http://www.example.com'); 206 | 207 | test('successfull', () async { 208 | var client = authenticatedClient( 209 | mockClient(expectAsync1((request) { 210 | expect(request.method, equals('POST')); 211 | expect(request.url, equals(url)); 212 | expect(request.headers.length, equals(1)); 213 | expect(request.headers['Authorization'], equals('Bearer bar')); 214 | 215 | return new Future.value(new Response('', 204)); 216 | }), expectClose: false), 217 | credentials); 218 | expect(client.credentials, equals(credentials)); 219 | 220 | var response = await client.send(new RequestImpl('POST', url)); 221 | expect(response.statusCode, equals(204)); 222 | }); 223 | 224 | test('access-denied', () { 225 | var client = authenticatedClient( 226 | mockClient(expectAsync1((request) { 227 | expect(request.method, equals('POST')); 228 | expect(request.url, equals(url)); 229 | expect(request.headers.length, equals(1)); 230 | expect(request.headers['Authorization'], equals('Bearer bar')); 231 | 232 | var headers = const {'www-authenticate': 'foobar'}; 233 | return new Future.value(new Response('', 401, headers: headers)); 234 | }), expectClose: false), 235 | credentials); 236 | expect(client.credentials, equals(credentials)); 237 | 238 | expect(client.send(new RequestImpl('POST', url)), 239 | throwsA(isAccessDeniedException)); 240 | }); 241 | 242 | test('non-bearer-token', () { 243 | var aToken = credentials.accessToken; 244 | var nonBearerCredentials = new AccessCredentials( 245 | new AccessToken('foobar', aToken.data, aToken.expiry), 246 | 'refresh', 247 | ['s1', 's2']); 248 | 249 | expect( 250 | () => authenticatedClient( 251 | mockClient(_defaultResponseHandler, expectClose: false), 252 | nonBearerCredentials), 253 | throwsA(isArgumentError)); 254 | }); 255 | }); 256 | 257 | group('autoRefreshingClient', () { 258 | var url = Uri.parse('http://www.example.com'); 259 | 260 | test('up-to-date', () async { 261 | var client = autoRefreshingClient( 262 | clientId, 263 | credentials, 264 | mockClient(expectAsync1((request) { 265 | return new Future.value(new Response('', 200)); 266 | }), expectClose: false)); 267 | expect(client.credentials, equals(credentials)); 268 | 269 | var response = await client.send(new RequestImpl('POST', url)); 270 | expect(response.statusCode, equals(200)); 271 | }); 272 | 273 | test('no-refresh-token', () { 274 | var credentials = new AccessCredentials( 275 | new AccessToken('Bearer', 'bar', yesterday), null, ['s1', 's2']); 276 | 277 | expect( 278 | () => autoRefreshingClient(clientId, credentials, 279 | mockClient(_defaultResponseHandler, expectClose: false)), 280 | throwsA(isArgumentError)); 281 | }); 282 | 283 | test('refresh-failed', () { 284 | var credentials = new AccessCredentials( 285 | new AccessToken('Bearer', 'bar', yesterday), 286 | 'refresh', 287 | ['s1', 's2']); 288 | 289 | var client = autoRefreshingClient( 290 | clientId, 291 | credentials, 292 | mockClient(expectAsync1((request) { 293 | // This should be a refresh request. 294 | expect(request.headers['foo'], isNull); 295 | return refreshErrorResponse(request); 296 | }), expectClose: false)); 297 | expect(client.credentials, equals(credentials)); 298 | 299 | var request = new RequestImpl('POST', url); 300 | request.headers.addAll({'foo': 'bar'}); 301 | expect(client.send(request), throwsA(isRefreshFailedException)); 302 | }); 303 | 304 | test('invalid-content-type', () { 305 | var credentials = new AccessCredentials( 306 | new AccessToken('Bearer', 'bar', yesterday), 307 | 'refresh', 308 | ['s1', 's2']); 309 | 310 | var client = autoRefreshingClient( 311 | clientId, 312 | credentials, 313 | mockClient(expectAsync1((request) { 314 | // This should be a refresh request. 315 | expect(request.headers['foo'], isNull); 316 | var headers = {'content-type': 'image/png'}; 317 | 318 | return new Future.value(new Response('', 200, headers: headers)); 319 | }), expectClose: false)); 320 | expect(client.credentials, equals(credentials)); 321 | 322 | var request = new RequestImpl('POST', url); 323 | request.headers.addAll({'foo': 'bar'}); 324 | expect(client.send(request), throwsA(isException)); 325 | }); 326 | 327 | test('successful-refresh', () async { 328 | int serverInvocation = 0; 329 | 330 | var credentials = new AccessCredentials( 331 | new AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1']); 332 | 333 | var client = autoRefreshingClient( 334 | clientId, 335 | credentials, 336 | mockClient( 337 | expectAsync1((request) { 338 | if (serverInvocation++ == 0) { 339 | // This should be a refresh request. 340 | expect(request.headers['foo'], isNull); 341 | return successfulRefresh(request); 342 | } else { 343 | // This is the real request. 344 | expect(request.headers['foo'], equals('bar')); 345 | return new Future.value(new Response('', 200)); 346 | } 347 | }, count: 2), 348 | expectClose: false)); 349 | expect(client.credentials, equals(credentials)); 350 | 351 | bool executed = false; 352 | client.credentialUpdates.listen(expectAsync1((newCredentials) { 353 | expect(newCredentials.accessToken.type, equals('Bearer')); 354 | expect(newCredentials.accessToken.data, equals('atoken')); 355 | executed = true; 356 | }), onDone: expectAsync0(() {})); 357 | 358 | var request = new RequestImpl('POST', url); 359 | request.headers.addAll({'foo': 'bar'}); 360 | 361 | var response = await client.send(request); 362 | expect(response.statusCode, equals(200)); 363 | 364 | // The `client.send()` will have triggered a credentials refresh. 365 | expect(executed, isTrue); 366 | 367 | client.close(); 368 | }); 369 | }); 370 | }); 371 | } 372 | 373 | final _jsonContentType = const {'content-type': 'application/json'}; 374 | -------------------------------------------------------------------------------- /test/test_utils.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014, 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 googleapis_auth.test_utils; 6 | 7 | import 'dart:async'; 8 | 9 | import 'package:googleapis_auth/auth.dart'; 10 | import 'package:googleapis_auth/src/crypto/pem.dart'; 11 | import 'package:googleapis_auth/src/utils.dart'; 12 | import 'package:http/http.dart'; 13 | import 'package:http/testing.dart'; 14 | import 'package:test/test.dart'; 15 | 16 | const Matcher isUserConsentException = 17 | const TypeMatcher(); 18 | 19 | const Matcher isRefreshFailedException = 20 | const TypeMatcher(); 21 | 22 | const Matcher isAccessDeniedException = 23 | const TypeMatcher(); 24 | 25 | const Matcher isTransportException = const TypeMatcher(); 26 | 27 | class TransportException implements Exception {} 28 | 29 | Client get transportFailure { 30 | return new MockClient(expectAsync1((Request _) { 31 | return new Future.error(new TransportException()); 32 | })); 33 | } 34 | 35 | const TestPrivateKeyString = '''-----BEGIN RSA PRIVATE KEY----- 36 | MIIEowIBAAKCAQEAuDOwXO14ltE1j2O0iDSuqtbw/1kMKjeiki3oehk2zNoUte42 37 | /s2rX15nYCkKtYG/r8WYvKzb31P4Uow1S4fFydKNWxgX4VtEjHgeqfPxeCL9wiJc 38 | 9KkEt4fyhj1Jo7193gCLtovLAFwPzAMbFLiXWkfqalJ5Z77fOE4Mo7u4pEgxNPgL 39 | VFGe0cEOAsHsKlsze+m1pmPHwWNVTcoKe5o0hOzy6hCPgVc6me6Y7aO8Fb4OVg0l 40 | XQdQpWn2ikVBpzBcZ6InnYyJ/CJNa3WL1LJ65mmYnfHtKGoMqhLK48OReguwRwwF 41 | e9/2+8UcdZcN5rsvt7yg3ZrKNH8rx+wZ36sRewIDAQABAoIBAQCn1HCcOsHkqDlk 42 | rDOQ5m8+uRhbj4bF8GrvRWTL2q1TeF/mY2U4Q6wg+KK3uq1HMzCzthWz0suCb7+R 43 | dq4YY1ySxoSEuy8G5WFPmyJVNy6Lh1Yty6FmSZlCn1sZdD3kMoK8A0NIz5Xmffrm 44 | pu3Fs2ozl9K9jOeQ3xgC9RoPFLrm8lHJ45Vn+SnTxZnsXT6pwpg3TnFIx5ZinU8k 45 | l0Um1n80qD2QQDakQ5jyr2odAELLBDlyCkxAglBXAVt4nk9Kl6nxb4snd9dnrL70 46 | WjLynWQsDczaV9TZIl2hYkMud+9OLVlUUtB+0c5b0p2t2P0sLltDaq3H6pT6yu2G 47 | 8E86J9IBAoGBAPJaTNV5ysVOFn+YwWwRztzrvNArUJkVq8abN0gGp3gUvDEZnvzK 48 | weF7+lfZzcwVRmQkL3mWLzzZvCx77RfulAzLi5iFuRBPhhhxAPDiDuyL9B7O81G/ 49 | M/W5DPctGOyD/9cnLuh72oij0unc5MLSfzJf8wblpcjJnPBDqIVh6Qt9AoGBAMKT 50 | Gacf4iSj1xW+0wrnbZlDuyCl6Msptj8ePcvLQrFqQmBwsXmWgVR+gFc/1G3lRft0 51 | QC6chsmafQHIIPpaDjq3sQ01/tUu7LXL+g/Hw9XtUHbkg3sZIQBtC26rKdStfHNS 52 | KTvuCgn/dAJNjiohfhWMt9R4Q6E5FV6PqQHJzPJXAoGAC41qZDKuC8GxKNvrPG+M 53 | 4NML6RBngySZT5pOhExs5zh10BFclshDfbAfOtjTCotpE5T1/mG+VrQ6WBSANMfW 54 | ntWFDfwx2ikwRzH7zX+5HmV9eYp75sWqgGgVyiKIMZ4JMARaJBLjU+gbQbKZ5P+L 55 | uKcCOq3vvSZ/KKTQ/6qvJTECgYBiWgbCgoxF5wdmd4Gn5llw+lqRYyur3hbACuJD 56 | rCe3FDYfF3euNRSEiDkJYTtYnWbldtqmdPpw14VOrEF3KqQ8q/Nz8RIx4jlGn6dz 57 | 6I8mCIH+xv1q8MXMuFHqC9zmIxdgF2y+XVF3wkd6jodI5oscC3g0juHokbkqhkVw 58 | oPfWmwKBgBfR6jv0gWWeWTfkNwj+cMLHQV1uvz6JyLH5K4iISEDFxYkd37jrHB8A 59 | 9hz9UDfmCbSs2j8CXDg7zCayM6tfu4Vtx+8S5g3oN6sa1JXFY1Os7SoXhTfX9M+7 60 | QpYYDJZwkgZrVQoKMIdCs9xfyVhZERq945NYLekwE1t2W+tOVBgR 61 | -----END RSA PRIVATE KEY-----'''; 62 | 63 | final testPrivateKey = keyFromString(TestPrivateKeyString); 64 | 65 | expectExpiryOneHourFromNow(AccessToken accessToken) { 66 | var now = new DateTime.now().toUtc(); 67 | var diff = accessToken.expiry.difference(now).inSeconds - 68 | (3600 - MAX_EXPECTED_TIMEDIFF_IN_SECONDS); 69 | expect(-2 <= diff && diff <= 2, isTrue); 70 | } 71 | 72 | Client mockClient(Future requestHandler(Request _), 73 | {bool expectClose: true}) { 74 | return new ExpectCloseMockClient(requestHandler, expectClose ? 1 : 0); 75 | } 76 | 77 | /// A client which will keep the VM alive until `close()` was called. 78 | class ExpectCloseMockClient extends MockClient { 79 | late Function _expectedToBeCalled; 80 | 81 | ExpectCloseMockClient(Future requestHandler(Request _), int c) 82 | : super(requestHandler) { 83 | _expectedToBeCalled = expectAsync0(() {}, count: c); 84 | } 85 | 86 | void close() { 87 | super.close(); 88 | _expectedToBeCalled(); 89 | } 90 | } 91 | --------------------------------------------------------------------------------