Bring googleapis_auth into the mono repo (#140)
Synced at https://github.com/dart-lang/googleapis_auth/commit/30c084b7650fbd3e52525a127e0e65fc528e85a8
Then fixed enabled lints, repo lint in pubspec, etc
diff --git a/googleapis_auth/CHANGELOG.md b/googleapis_auth/CHANGELOG.md
new file mode 100644
index 0000000..e0947c1
--- /dev/null
+++ b/googleapis_auth/CHANGELOG.md
@@ -0,0 +1,106 @@
+## 0.2.12+2-dev
+
+## 0.2.12+1
+
+ * Removed a `dart:async` import that isn't required for \>=Dart 2.1.
+ * Require \>=Dart 2.1.
+
+## 0.2.12
+ * Add `clientViaApplicationDefaultCredentials` for obtaining credentials using
+ [ADC](https://cloud.google.com/docs/authentication/production).
+
+## 0.2.11+1
+ * Fix 'multiple completer completion' bug in `ImplicitFlow`.
+
+## 0.2.11
+ * Add the `force` parameter to the `obtainAccessCredentialsViaUserConsent` API.
+
+## 0.2.10
+ * Look for GCE metadata host in environment under `$GCE_METADATA_HOST`.
+
+## 0.2.9
+ * Prepare for [Uint8List SDK breaking change](Prepare for Uint8List SDK breaking change).
+
+## 0.2.8
+
+* Initialize implicit browser flows statically, allowing multiple ImplicitFlow
+ objects to initialize without trying to load the gapi JavaScript library
+ multiple times.
+
+## 0.2.7
+
+ - Support for specifying desired `ResponseType`, allowing applications to
+ obtain an `id_token` using `ImplicitBrowserFlow`.
+
+## 0.2.6
+
+- Ignore script loading error after timeout for in-browser implicit login-flow.
+
+## 0.2.5+3
+
+- Support `package:http` `>=0.11.3+17 <0.13.0`.
+
+## 0.2.5+2
+
+* Support Dart 2.
+
+## 0.2.5+1
+
+* Switch all uppercase constants from `dart:convert` to lowercase.
+
+## 0.2.5
+
+* Add an optional `loginHint` parameter to browser oauth2 flow APIs which can be
+ used to specify a hint as to which user is being logged in.
+
+## 0.2.4
+
+* Added `id_token` to `AccessCredentials`
+
+* Migrated to Dart 2 `BigInt`.
+
+## 0.2.3+6
+
+- Fix async issue in oauth2 flow implementation
+
+## 0.2.3+5
+
+- Support the latest version of `crypto` package.
+
+## 0.2.3+4
+
+- Make package strong-mode compliant.
+
+## 0.2.3+3
+
+- Support package:crypto >= 0.9.2
+
+## 0.2.3+2
+
+- Use preferred "Metadata-Flavor" HTTP header in
+ `MetadataServerAuthorizationFlow` instead of the deprecated
+ "X-Google-Metadata-Request" header.
+
+## 0.2.3
+
+- Allow `ServiceAccountCredentials` constructors to take an optional
+ `user` argument to specify a user to impersonate.
+
+## 0.2.2
+
+- Allow `ServiceAccountCredentials.fromJson` to accept a `Map`.
+- Cleaned up `README.md`
+
+## 0.2.1
+- Added optional `force` and `immediate` arguments to `runHybridFlow`.
+
+## 0.2.0
+- Renamed `forceUserConsent` parameter to `immediate`.
+- Added `runHybridFlow` function to `auth_browser`, with corresponding
+ `HybridFlowResult` class.
+
+## 0.1.1
+- Add `clientViaApiKey` functions to `auth_io` ad `auth_browser`.
+
+## 0.1.0
+- First release.
diff --git a/googleapis_auth/LICENSE b/googleapis_auth/LICENSE
new file mode 100644
index 0000000..5c60afe
--- /dev/null
+++ b/googleapis_auth/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2014, the Dart project authors. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of Google Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/googleapis_auth/README.md b/googleapis_auth/README.md
new file mode 100644
index 0000000..ea03d3f
--- /dev/null
+++ b/googleapis_auth/README.md
@@ -0,0 +1,305 @@
+## Googleapis Auth
+
+This package provides support for obtaining OAuth2 credentials to access
+Google APIs.
+
+This package also provides convenience functionality for:
+- obtaining authenticated HTTP clients
+- automatically refreshing OAuth2 credentials
+
+### Using this package
+
+Using this package requires creating a Google Cloud Project and obtaining
+application credentials for the specific application type.
+The steps required are:
+
+- Create a new Google Cloud Project on the
+ [Google Developers Console](https://console.developers.google.com)
+- Enable all APIs that the application will use on the
+ [Google Developers Console](https://console.developers.google.com)
+ (under DevConsole -> Project -> APIs & auth -> APIs)
+- Obtain application credentials for a specific application type on the
+ [Google Developers Console](https://console.developers.google.com)
+ (under DevConsole -> Project -> APIs & auth -> Credentials)
+- Use the `googleapis_auth` package to obtain access credentials /
+ obtain an authenticated HTTP client.
+
+Depending on the application type, there are different ways to achieve the
+third and fourth step. The following is a list of supported OAuth2 flows with
+a description of these two steps.
+
+
+#### Client-side Web Application
+
+For client-side only web applications a "Client ID" needs to be created
+(under DevConsole -> Project -> APIs & auth -> Credentials). When
+creating a new client ID, select the "Web application" type. For client-side
+only applications, no `Redirect URIs` are necessary. The `Javascript Origins`
+setting must be set to all URLs on which your application
+will be served (e.g. http://localhost:8080 for local testing).
+
+
+After the Client Id has been created, you can obtain access credentials via
+```dart
+import "package:googleapis_auth/auth_browser.dart";
+
+...
+
+var id = new ClientId("....apps.googleusercontent.com", null);
+var scopes = [...];
+
+// Initialize the browser oauth2 flow functionality.
+createImplicitBrowserFlow(id, scopes).then((BrowserOAuth2Flow flow) {
+ flow.obtainAccessCredentialsViaUserConsent()
+ .then((AccessCredentials credentials) {
+ // Credentials are available in [credentials].
+ ...
+ flow.close();
+ });
+});
+```
+
+or obtain an authenticated HTTP client via
+```dart
+import "package:googleapis_auth/auth_browser.dart";
+
+...
+
+var id = new ClientId("....apps.googleusercontent.com", null);
+var scopes = [...];
+
+// Initialize the browser oauth2 flow functionality.
+createImplicitBrowserFlow(id, scopes).then((BrowserOAuth2Flow flow) {
+ flow.clientViaUserConsent().then((AuthClient client) {
+ // Authenticated and auto refreshing client is available in [client].
+ ...
+ client.close();
+ flow.close();
+ });
+});
+```
+
+To prevent popup blockers from blocking the user authorization dialog, the
+methods `obtainAccessCredentialsViaUserConsent` and `clientViaUserConsent`
+should preferably only be called inside an event handler, since most browsers
+do not block popup windows created in response to a user interaction.
+
+The authenticated HTTP client can now access data on behalf a user for the
+requested oauth2 scopes.
+
+
+#### Installed/Console Application
+
+For installed/console applications a "Client ID" needs to be created
+(under DevConsole -> Project -> APIs & auth -> Credentials). When
+creating a new client ID, select the "Installed application -> Other" type.
+
+The redirect URIs for the automatic and manual flow will be configured
+automatically.
+
+After the Client Id has been created, you can obtain access credentials via
+```dart
+import "package:http/http.dart" as http;
+import "package:googleapis_auth/auth_io.dart";
+
+...
+
+var id = new ClientId("....apps.googleusercontent.com", "...");
+var scopes = [...];
+
+var client = new http.Client();
+obtainAccessCredentialsViaUserConsent(id, scopes, client, prompt)
+ .then((AccessCredentials credentials) {
+ // Access credentials are available in [credentials].
+ // ...
+ client.close();
+});
+
+void prompt(String url) {
+ print("Please go to the following URL and grant access:");
+ print(" => $url");
+ print("");
+}
+```
+
+or obtain an authenticated HTTP client via
+
+```dart
+import "package:googleapis_auth/auth_io.dart";
+
+...
+
+var id = new ClientId("....apps.googleusercontent.com", "...");
+var scopes = [...];
+
+clientViaUserConsent(id, scopes, prompt).then((AuthClient client) {
+ // Authenticated and auto refreshing client is available in [client].
+ // ...
+ client.close();
+});
+
+void prompt(String url) {
+ print("Please go to the following URL and grant access:");
+ print(" => $url");
+ print("");
+}
+```
+
+In case of misconfigured browsers/proxies or other issues, it is also possible
+to use a manual flow via `obtainAccessCredentialsViaUserConsentManual` and
+`clientViaUserConsentManual`. But in this case the `prompt` function needs to
+complete with a `Future<String>` which contains the "authorization code".
+The user obtains the "authorization code" (which is a string of characters) in
+a browser and needs to copy & paste it to the application. (The prompt function
+should block until it has gotten the "authorization code" from the user.)
+
+The authenticated HTTP client can now access data on behalf a user for the
+requested oauth2 scopes.
+
+
+#### Autonomous Application / Service Account
+
+If an application wants to act autonomously and access e.g. data from a Google
+Cloud Project, then a Service Account can be created. In this case no user
+authorization is involved.
+
+A service account can be created via the "Service account" application type
+when creating a new Client ID
+(under DevConsole -> Project -> APIs & auth -> Credentials). It will download
+a JSON document which contains a private RSA key. That private key is used for
+obtaining access credentials.
+
+After the service account was created, you can obtain access credentials via
+```dart
+import "package:http/http.dart" as http;
+import "package:googleapis_auth/auth_io.dart";
+
+var accountCredentials = new ServiceAccountCredentials.fromJson({
+ "private_key_id": "<please fill in>",
+ "private_key": "<please fill in>",
+ "client_email": "<please fill in>@developer.gserviceaccount.com",
+ "client_id": "<please fill in>.apps.googleusercontent.com",
+ "type": "service_account"
+});
+var scopes = [...];
+
+...
+
+var client = new http.Client();
+obtainAccessCredentialsViaServiceAccount(accountCredentials, scopes, client)
+ .then((AccessCredentials credentials) {
+ // Access credentials are available in [credentials].
+ // ...
+ client.close();
+});
+```
+
+or an authenticated HTTP client via
+
+```dart
+import "package:googleapis_auth/auth_io.dart";
+
+final accountCredentials = new ServiceAccountCredentials.fromJson({
+ "private_key_id": "<please fill in>",
+ "private_key": "<please fill in>",
+ "client_email": "<please fill in>@developer.gserviceaccount.com",
+ "client_id": "<please fill in>.apps.googleusercontent.com",
+ "type": "service_account"
+});
+var scopes = [...];
+
+...
+
+clientViaServiceAccount(accountCredentials, scopes).then((AuthClient client) {
+ // [client] is an authenticated HTTP client.
+ // ...
+ client.close();
+});
+```
+
+The authenticated HTTP client can now access APIs.
+
+##### Impersonation
+
+For some APIs the use of a service account also requires to impersonate a
+user. To support that the `ServiceAccountCredentials` constructors have an
+optional argument `impersonatedUser` to specify the user to impersonate.
+
+One example of this are the Google Apps APIs. See [Perform Google Apps
+Domain-Wide Delegation of Authority]
+(https://developers.google.com/admin-sdk/directory/v1/guides/delegation)
+for information on the additional security configuration required to
+enable this for a service account.
+
+
+#### Autonomous Application / Compute Engine using metadata service
+
+If an application wants to act autonomously and access e.g. data from a Google
+Cloud Project, then a Service Account can be used. In case the application is
+running on a ComputeEngine VM it is possible to start a VM with a set of scopes
+the VM is allowed to use. See the
+[documentation](https://developers.google.com/compute/docs/authentication#using)
+for further information.
+
+Here is an example of using the metadata service for obtaining access
+credentials on a ComputeEngine VM.
+```dart
+import "package:http/http.dart" as http;
+import "package:googleapis_auth/auth_io.dart";
+
+var client = new http.Client();
+obtainAccessCredentialsViaMetadataServer(client)
+ .then((AccessCredentials credentials) {
+ // Access credentials are available in [credentials].
+ // ...
+ client.close();
+});
+```
+
+or an authenticated HTTP client via
+
+```dart
+import "package:googleapis_auth/auth_io.dart";
+
+clientViaMetadataServer().then((AuthClient client) {
+ // [client] is an authenticated HTTP client.
+ // ...
+ client.close();
+});
+```
+The authenticated HTTP client can now access APIs.
+
+
+#### Accessing Public Data with API Key
+
+It is possible to access some APIs by just using an API key without OAuth2.
+
+A API key can be obtained on the Google Developers Console by creating a Key
+at the "Public API access" section
+(under DevConsole -> Project -> APIs & auth -> Credentials).
+
+A key can be created for different application types: For browser applications
+it is necessary to specify a set of referer URls from which the application
+would like to access APIs. For server applications it is possible to specify
+a list of IP ranges from which the client application would like to access APIs.
+
+Note that the ApiKey is used for quota and billing purposes and should not be
+disclosed to third parties.
+
+Here is an example of getting an HTTP client which uses an API key for making
+HTTP requests.
+
+```dart
+import "package:googleapis_auth/auth_io.dart";
+
+var client = clientViaApiKey('<api-key-from-devconsole>');
+// [client] can now be used to make REST calls to Google APIs.
+// ...
+client.close();
+```
+
+### More information
+
+More information can be obtained from official Google Developers documentation:
+- [OAuth2 to Access Google APIs](https://developers.google.com/accounts/docs/OAuth2?hl=fr)
+- [OAuth2 Playground](https://developers.google.com/oauthplayground/)
diff --git a/googleapis_auth/lib/auth.dart b/googleapis_auth/lib/auth.dart
new file mode 100644
index 0000000..91a8d06
--- /dev/null
+++ b/googleapis_auth/lib/auth.dart
@@ -0,0 +1,297 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.auth;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:http/http.dart';
+
+import 'src/auth_http_utils.dart';
+import 'src/crypto/pem.dart';
+import 'src/crypto/rsa.dart';
+import 'src/http_client_base.dart';
+import 'src/utils.dart';
+
+/// An OAuth2 access token.
+class AccessToken {
+ /// The token type, usually "Bearer"
+ final String type;
+
+ /// The access token data.
+ final String data;
+
+ /// Time at which the token will be expired (UTC time)
+ final DateTime expiry;
+
+ /// [expiry] must be a UTC `DateTime`.
+ AccessToken(this.type, this.data, this.expiry) {
+ if (!expiry.isUtc) {
+ throw ArgumentError('The expiry date must be a Utc DateTime.');
+ }
+ }
+
+ bool get hasExpired => DateTime.now().toUtc().isAfter(expiry);
+
+ @override
+ String toString() => 'AccessToken(type=$type, data=$data, expiry=$expiry)';
+}
+
+/// OAuth2 Credentials.
+class AccessCredentials {
+ /// An access token.
+ final AccessToken accessToken;
+
+ /// A refresh token, which can be used to refresh the access credentials.
+ final String? refreshToken;
+
+ /// A JWT used in calls to Google APIs that accept an id_token param.
+ final String? idToken;
+
+ /// Scopes these credentials are valid for.
+ final List<String> scopes;
+
+ AccessCredentials(
+ this.accessToken,
+ this.refreshToken,
+ this.scopes, {
+ this.idToken,
+ });
+}
+
+/// Represents the client application's credentials.
+class ClientId {
+ /// The identifier used to identify this application to the server.
+ final String identifier;
+
+ /// The client secret used to identify this application to the server.
+ final String? secret;
+
+ ClientId(this.identifier, this.secret);
+
+ ClientId.serviceAccount(this.identifier) : secret = null;
+}
+
+/// Represents credentials for a service account.
+class ServiceAccountCredentials {
+ /// The email address of this service account.
+ final String email;
+
+ /// The clientId.
+ final ClientId clientId;
+
+ /// Private key.
+ final String privateKey;
+
+ /// Impersonated user, if any. If not impersonating any user this is `null`.
+ final String? impersonatedUser;
+
+ /// Private key as an [RSAPrivateKey].
+ final RSAPrivateKey privateRSAKey;
+
+ /// Creates a new [ServiceAccountCredentials] from JSON.
+ ///
+ /// [json] can be either a [Map] or a JSON map encoded as a [String].
+ ///
+ /// The optional named argument [impersonatedUser] is used to set the user
+ /// to impersonate if impersonating a user.
+ factory ServiceAccountCredentials.fromJson(json, {String? impersonatedUser}) {
+ if (json is String) {
+ json = jsonDecode(json);
+ }
+ if (json is! Map) {
+ throw ArgumentError('json must be a Map or a String encoding a Map.');
+ }
+ final identifier = json['client_id'];
+ final privateKey = json['private_key'];
+ final email = json['client_email'];
+ final type = json['type'];
+
+ if (type != 'service_account') {
+ throw ArgumentError('The given credentials are not of type '
+ 'service_account (was: $type).');
+ }
+
+ if (identifier == null || privateKey == null || email == null) {
+ throw ArgumentError('The given credentials do not contain all the '
+ 'fields: client_id, private_key and client_email.');
+ }
+
+ final clientId = ClientId(identifier, null);
+ return ServiceAccountCredentials(email, clientId, privateKey,
+ impersonatedUser: impersonatedUser);
+ }
+
+ /// Creates a new [ServiceAccountCredentials].
+ ///
+ /// [email] is the e-mail address of the service account.
+ ///
+ /// [clientId] is the client ID for the service account.
+ ///
+ /// [privateKey] is the base 64 encoded, unencrypted private key, including
+ /// the '-----BEGIN PRIVATE KEY-----' and '-----END PRIVATE KEY-----'
+ /// boundaries.
+ ///
+ /// The optional named argument [impersonatedUser] is used to set the user
+ /// to impersonate if impersonating a user is needed.
+ ServiceAccountCredentials(
+ this.email,
+ this.clientId,
+ this.privateKey, {
+ this.impersonatedUser,
+ }) : privateRSAKey = keyFromString(privateKey);
+}
+
+/// A authenticated HTTP client.
+abstract class AuthClient implements Client {
+ /// The credentials currently used for making HTTP requests.
+ AccessCredentials get credentials;
+}
+
+/// A autorefreshing, authenticated HTTP client.
+abstract class AutoRefreshingAuthClient implements AuthClient {
+ /// A broadcast stream of [AccessCredentials].
+ ///
+ /// A listener will get notified when new [AccessCredentials] were obtained.
+ Stream<AccessCredentials> get credentialUpdates;
+}
+
+/// Thrown if an attempt to refresh a token failed.
+class RefreshFailedException implements Exception {
+ final String message;
+
+ RefreshFailedException(this.message);
+
+ @override
+ String toString() => message;
+}
+
+/// Thrown if an attempt to make an authorized request failed.
+class AccessDeniedException implements Exception {
+ final String message;
+
+ AccessDeniedException(this.message);
+
+ @override
+ String toString() => message;
+}
+
+/// Thrown if user did not give his consent.
+class UserConsentException implements Exception {
+ final String message;
+
+ UserConsentException(this.message);
+
+ @override
+ String toString() => message;
+}
+
+/// Obtain an `http.Client` which automatically authenticates
+/// requests using [credentials].
+///
+/// Note that the returned `RequestHandler` will not auto-refresh the given
+/// [credentials].
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+AuthClient authenticatedClient(
+ Client baseClient, AccessCredentials credentials) {
+ if (credentials.accessToken.type != 'Bearer') {
+ throw ArgumentError('Only Bearer access tokens are accepted.');
+ }
+ return AuthenticatedClient(baseClient, credentials);
+}
+
+/// Obtain an `http.Client` which automatically refreshes [credentials]
+/// before they expire. Uses [baseClient] as a base for making authenticated
+/// http requests and for refreshing [credentials].
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+AutoRefreshingAuthClient autoRefreshingClient(
+ ClientId clientId, AccessCredentials credentials, Client baseClient) {
+ if (credentials.accessToken.type != 'Bearer') {
+ throw ArgumentError('Only Bearer access tokens are accepted.');
+ }
+ if (credentials.refreshToken == null) {
+ throw ArgumentError('Refresh token in AccessCredentials was `null`.');
+ }
+ return AutoRefreshingClient(baseClient, clientId, credentials);
+}
+
+/// Tries to obtain refreshed [AccessCredentials] based on [credentials] using
+/// [client].
+Future<AccessCredentials> refreshCredentials(
+ ClientId clientId, AccessCredentials credentials, Client client) async {
+ final formValues = [
+ 'client_id=${Uri.encodeComponent(clientId.identifier)}',
+ 'client_secret=${Uri.encodeComponent(clientId.secret!)}',
+ 'refresh_token=${Uri.encodeComponent(credentials.refreshToken!)}',
+ 'grant_type=refresh_token',
+ ];
+
+ final body =
+ Stream<List<int>>.fromIterable([(ascii.encode(formValues.join('&')))]);
+ final request = RequestImpl('POST', _googleTokenUri, body);
+ request.headers['content-type'] = 'application/x-www-form-urlencoded';
+
+ final response = await client.send(request);
+ var contentType = response.headers['content-type'];
+ contentType = contentType?.toLowerCase();
+
+ if (contentType == null ||
+ (!contentType.contains('json') && !contentType.contains('javascript'))) {
+ await response.stream.drain().catchError((_) {});
+ throw Exception('Server responded with invalid content type: $contentType. '
+ 'Expected json response.');
+ }
+
+ final jsonMap = await response.stream
+ .transform(ascii.decoder)
+ .transform(json.decoder)
+ .first as Map;
+
+ final idToken = jsonMap['id_token'];
+ final token = jsonMap['access_token'];
+ final seconds = jsonMap['expires_in'];
+ final tokenType = jsonMap['token_type'];
+ final error = jsonMap['error'];
+
+ if (response.statusCode != 200 && error != null) {
+ throw RefreshFailedException('Refreshing attempt failed. '
+ 'Response was ${response.statusCode}. Error message was $error.');
+ }
+
+ if (token == null || seconds is! int || tokenType != 'Bearer') {
+ throw Exception('Refresing attempt failed. '
+ 'Invalid server response.');
+ }
+
+ return AccessCredentials(AccessToken(tokenType, token, expiryDate(seconds)),
+ credentials.refreshToken, credentials.scopes,
+ idToken: idToken);
+}
+
+/// Available response types that can be requested when using the implicit
+/// browser login flow.
+///
+/// More information about these values can be found here:
+/// https://developers.google.com/identity/protocols/OpenIDConnect#response-type
+enum ResponseType {
+ /// Requests an access code. This triggers the basic rather than the implicit
+ /// flow.
+ code,
+
+ /// Requests the user's identity token when running the implicit flow.
+ idToken,
+
+ /// Requests the user's current permissions.
+ permission,
+
+ /// Requests the user's access token when running the implicit flow.
+ token,
+}
+
+final _googleTokenUri = Uri.parse('https://accounts.google.com/o/oauth2/token');
diff --git a/googleapis_auth/lib/auth_browser.dart b/googleapis_auth/lib/auth_browser.dart
new file mode 100644
index 0000000..3d3bb54
--- /dev/null
+++ b/googleapis_auth/lib/auth_browser.dart
@@ -0,0 +1,280 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.auth_browser;
+
+import 'dart:async';
+
+import 'package:http/browser_client.dart';
+import 'package:http/http.dart';
+
+import 'auth.dart';
+import 'src/auth_http_utils.dart';
+import 'src/http_client_base.dart';
+import 'src/oauth2_flows/implicit.dart';
+
+export 'auth.dart';
+
+/// Obtains a HTTP client which uses the given [apiKey] for making HTTP
+/// requests.
+///
+/// Note that the returned client should *only* be used for making HTTP requests
+/// to Google Services. The [apiKey] should not be disclosed to third parties.
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+Client clientViaApiKey(String apiKey, {Client? baseClient}) {
+ if (baseClient == null) {
+ baseClient = BrowserClient();
+ } else {
+ baseClient = nonClosingClient(baseClient);
+ }
+ return ApiKeyClient(baseClient, apiKey);
+}
+
+/// Will create and complete with a [BrowserOAuth2Flow] object.
+///
+/// This function will perform an implicit browser based oauth2 flow.
+///
+/// It will load Google's `gapi` library and initialize it. After initialization
+/// it will complete with a [BrowserOAuth2Flow] object. The flow object can be
+/// used to obtain `AccessCredentials` or an authenticated HTTP client.
+///
+/// If loading or initializing the `gapi` library results in an error, this
+/// future will complete with an error.
+///
+/// If [baseClient] is not given, one will be automatically created. It will be
+/// used for making authenticated HTTP requests. See [BrowserOAuth2Flow].
+///
+/// The [ClientId] can be obtained in the Google Cloud Console.
+///
+/// The user is responsible for closing the returned [BrowserOAuth2Flow] object.
+/// Closing the returned [BrowserOAuth2Flow] will not close [baseClient]
+/// if one was given.
+Future<BrowserOAuth2Flow> createImplicitBrowserFlow(
+ ClientId clientId, List<String> scopes,
+ {Client? baseClient}) {
+ final refCountedClient = baseClient == null
+ ? RefCountedClient(BrowserClient())
+ : RefCountedClient(baseClient, initialRefCount: 2);
+
+ final flow = ImplicitFlow(clientId.identifier, scopes);
+ return flow.initialize().catchError((error, stack) {
+ refCountedClient.close();
+ return Future.error(error, stack);
+ }).then((_) => BrowserOAuth2Flow._(flow, refCountedClient));
+}
+
+/// Used for obtaining oauth2 access credentials.
+///
+/// Warning:
+///
+/// The methods `obtainAccessCredentialsViaUserConsent` and
+/// `clientViaUserConsent` try to open a popup window for the user authorization
+/// dialog.
+///
+/// In order to prevent browsers from blocking the popup window, these
+/// methods should only be called inside an event handler, since most
+/// browsers do not block popup windows created in response to a user
+/// interaction.
+class BrowserOAuth2Flow {
+ final ImplicitFlow _flow;
+ final RefCountedClient _client;
+
+ bool _wasClosed = false;
+
+ /// The HTTP client passed in will be closed if `close` was called and all
+ /// generated HTTP clients via [clientViaUserConsent] were closed.
+ BrowserOAuth2Flow._(this._flow, this._client);
+
+ /// Obtain oauth2 [AccessCredentials].
+ ///
+ /// If [immediate] is `true` there will be no user involvement. If the user
+ /// is either not logged in or has not already granted the application access,
+ /// a `UserConsentException` will be thrown.
+ ///
+ /// If [immediate] is `false` the user might be asked to login (if he is not
+ /// already logged in) and might get asked to grant the application access
+ /// (if the application hasn't been granted access before).
+ ///
+ /// If [force] is `true` this will create a popup window and ask the user to
+ /// grant the application offline access. In case the user is not already
+ /// logged in, he will be presented with an login dialog first.
+ ///
+ /// If [force] is `false` this will only create a popup window if the user
+ /// has not already granted the application access.
+ ///
+ /// If [loginHint] is not `null`, it will be passed to the server as a hint
+ /// to which user is being signed-in. This can e.g. be an email or a User ID
+ /// which might be used as pre-selection in the sign-in flow.
+ ///
+ /// If [responseTypes] is not `null` or empty, it will be sent to the server
+ /// to inform the server of the type of responses to reply with.
+ ///
+ /// The returned future will complete with `AccessCredentials` if the user
+ /// has given the application access to it's data. Otherwise the future will
+ /// complete with a `UserConsentException`.
+ ///
+ /// In case another error occurs the returned future will complete with an
+ /// `Exception`.
+ Future<AccessCredentials> obtainAccessCredentialsViaUserConsent(
+ {bool immediate = false,
+ bool force = false,
+ String? loginHint,
+ List<ResponseType>? responseTypes}) {
+ _ensureOpen();
+ return _flow.login(
+ force: force,
+ immediate: immediate,
+ loginHint: loginHint,
+ responseTypes: responseTypes);
+ }
+
+ /// Obtains [AccessCredentials] and returns an authenticated HTTP client.
+ ///
+ /// After obtaining access credentials, this function will return an HTTP
+ /// [Client]. HTTP requests made on the returned client will get an
+ /// additional `Authorization` header with the `AccessCredentials` obtained.
+ ///
+ /// In case the `AccessCredentials` expire, it will try to obtain new ones
+ /// without user consent.
+ ///
+ /// See [obtainAccessCredentialsViaUserConsent] for how credentials will be
+ /// obtained. Errors from [obtainAccessCredentialsViaUserConsent] will be let
+ /// through to the returned `Future` of this function and to the returned
+ /// HTTP client (in case of credential refreshes).
+ ///
+ /// The returned HTTP client will forward errors from lower levels via it's
+ /// `Future<Response>` or it's `Response.read()` stream.
+ ///
+ /// The user is responsible for closing the returned HTTP client.
+ Future<AutoRefreshingAuthClient> clientViaUserConsent(
+ {bool immediate = false, String? loginHint}) =>
+ obtainAccessCredentialsViaUserConsent(
+ immediate: immediate, loginHint: loginHint)
+ .then(_clientFromCredentials);
+
+ /// Obtains [AccessCredentials] and an authorization code which can be
+ /// exchanged for permanent access credentials.
+ ///
+ /// Use case:
+ /// A web application might want to get consent for accessing data on behalf
+ /// of a user. The client part is a dynamic webapp which wants to open a
+ /// popup which asks the user for consent. The webapp might want to use the
+ /// credentials to make API calls, but the server may want to have offline
+ /// access to user data as well.
+ ///
+ /// If [force] is `true` this will create a popup window and ask the user to
+ /// grant the application offline access. In case the user is not already
+ /// logged in, he will be presented with an login dialog first.
+ ///
+ /// If [force] is `false` this will only create a popup window if the user
+ /// has not already granted the application access. Please note that the
+ /// authorization code can only be exchanged for a refresh token if the user
+ /// had to grant access via the popup window. Otherwise the code exchange
+ /// will only give an access token.
+ ///
+ /// If [loginHint] is not `null`, it will be passed to the server as a hint
+ /// to which user is being signed-in. This can e.g. be an email or a User ID
+ /// which might be used as pre-selection in the sign-in flow.
+ ///
+ /// If [immediate] is `true` there will be no user involvement. If the user
+ /// is either not logged in or has not already granted the application access,
+ /// a `UserConsentException` will be thrown.
+ Future<HybridFlowResult> runHybridFlow(
+ {bool force = true, bool immediate = false, String? loginHint}) async {
+ _ensureOpen();
+ final result = await _flow.loginHybrid(
+ force: force, immediate: immediate, loginHint: loginHint);
+ return HybridFlowResult(this, result.credential, result.code);
+ }
+
+ /// Will close this [BrowserOAuth2Flow] object and the HTTP [Client] it is
+ /// using.
+ ///
+ /// The clients obtained via [clientViaUserConsent] will continue to work.
+ /// The client obtained via `newClient` of obtained [HybridFlowResult] objects
+ /// will continue to work.
+ ///
+ /// After this flow object and all obtained clients were closed the underlying
+ /// HTTP client will be closed as well.
+ ///
+ /// After calling this `close` method, calls to [clientViaUserConsent],
+ /// [obtainAccessCredentialsViaUserConsent] and to `newClient` on returned
+ /// [HybridFlowResult] objects will fail.
+ void close() {
+ _ensureOpen();
+ _wasClosed = true;
+ _client.close();
+ }
+
+ void _ensureOpen() {
+ if (_wasClosed) {
+ throw StateError('BrowserOAuth2Flow has already been closed.');
+ }
+ }
+
+ AutoRefreshingAuthClient _clientFromCredentials(AccessCredentials cred) {
+ _ensureOpen();
+ _client.acquire();
+ return _AutoRefreshingBrowserClient(_client, cred, _flow);
+ }
+}
+
+/// Represents the result of running a browser based hybrid flow.
+///
+/// The `credentials` field holds credentials which can be used on the client
+/// side. The `newClient` function can be used to make a new authenticated HTTP
+/// client using these credentials.
+///
+/// The `authorizationCode` can be sent to the server, which knows the
+/// "client secret" and can exchange it with long-lived access credentials.
+///
+/// See the `obtainAccessCredentialsViaCodeExchange` function in the
+/// `googleapis_auth.auth_io` library for more details on how to use the
+/// authorization code.
+class HybridFlowResult {
+ final BrowserOAuth2Flow _flow;
+
+ /// Access credentials for making authenticated HTTP requests.
+ final AccessCredentials credentials;
+
+ /// The authorization code received from the authorization endpoint.
+ ///
+ /// The auth code can be used to receive permanent access credentials.
+ /// This requires a confidential client which can keep a secret.
+ final String? authorizationCode;
+
+ HybridFlowResult(this._flow, this.credentials, this.authorizationCode);
+
+ AutoRefreshingAuthClient newClient() {
+ _flow._ensureOpen();
+ return _flow._clientFromCredentials(credentials);
+ }
+}
+
+class _AutoRefreshingBrowserClient extends AutoRefreshDelegatingClient {
+ @override
+ AccessCredentials credentials;
+ final ImplicitFlow _flow;
+ Client _authClient;
+
+ _AutoRefreshingBrowserClient(Client client, this.credentials, this._flow)
+ : _authClient = authenticatedClient(client, credentials),
+ super(client);
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) {
+ if (!credentials.accessToken.hasExpired) {
+ return _authClient.send(request);
+ } else {
+ return _flow.login(immediate: true).then((newCredentials) {
+ credentials = newCredentials;
+ notifyAboutNewCredentials(credentials);
+ _authClient = authenticatedClient(baseClient, credentials);
+ return _authClient.send(request);
+ });
+ }
+ }
+}
diff --git a/googleapis_auth/lib/auth_io.dart b/googleapis_auth/lib/auth_io.dart
new file mode 100644
index 0000000..86dc053
--- /dev/null
+++ b/googleapis_auth/lib/auth_io.dart
@@ -0,0 +1,411 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.auth_io;
+
+import 'dart:io';
+
+import 'package:http/http.dart';
+
+import 'auth.dart';
+import 'src/adc_utils.dart';
+import 'src/auth_http_utils.dart';
+import 'src/http_client_base.dart';
+import 'src/oauth2_flows/auth_code.dart';
+import 'src/oauth2_flows/jwt.dart';
+import 'src/oauth2_flows/metadata_server.dart';
+import 'src/typedefs.dart';
+
+export 'auth.dart';
+export 'src/typedefs.dart';
+
+/// Create a client using
+/// [Application Default Credentials](https://cloud.google.com/docs/authentication/production).
+///
+/// Looks for credentials in the following order of preference:
+/// 1. A JSON file whose path is specified by `GOOGLE_APPLICATION_CREDENTIALS`,
+/// this file typically contains [exported service account keys][svc-keys].
+/// 2. A JSON file created by
+/// [`gcloud auth application-default login`][gcloud-login]
+/// in a well-known location (`%APPDATA%/gcloud/application_default_credentials.json`
+/// on Windows and `$HOME/.config/gcloud/application_default_credentials.json` on Linux/Mac).
+/// 3. On Google Compute Engine and App Engine Flex we fetch credentials from
+/// [GCE metadata service][metadata].
+///
+/// [metadata]: https://cloud.google.com/compute/docs/storing-retrieving-metadata
+/// [svc-keys]: https://cloud.google.com/docs/authentication/getting-started
+/// [gcloud-login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
+Future<AutoRefreshingAuthClient> clientViaApplicationDefaultCredentials({
+ required List<String> scopes,
+ Client? baseClient,
+}) async {
+ if (baseClient == null) {
+ baseClient = Client();
+ } else {
+ baseClient = nonClosingClient(baseClient);
+ }
+
+ // If env var specifies a file to load credentials from we'll do that.
+ final credsEnv = Platform.environment['GOOGLE_APPLICATION_CREDENTIALS'];
+ if (credsEnv != null && credsEnv.isNotEmpty) {
+ // If env var is specific and not empty, we always try to load, even if
+ // the file doesn't exist.
+ return await fromApplicationsCredentialsFile(
+ File(credsEnv),
+ 'GOOGLE_APPLICATION_CREDENTIALS',
+ scopes,
+ baseClient,
+ );
+ }
+
+ // Attempt to use file created by `gcloud auth application-default login`
+ File credFile;
+ if (Platform.isWindows) {
+ credFile = File.fromUri(Uri.directory(Platform.environment['APPDATA']!)
+ .resolve('gcloud/application_default_credentials.json'));
+ } else {
+ credFile = File.fromUri(Uri.directory(Platform.environment['HOME']!)
+ .resolve('.config/gcloud/application_default_credentials.json'));
+ }
+ // Only try to load from credFile if it exists.
+ if (await credFile.exists()) {
+ return await fromApplicationsCredentialsFile(
+ credFile,
+ '`gcloud auth application-default login`',
+ scopes,
+ baseClient,
+ );
+ }
+
+ return await clientViaMetadataServer(baseClient: baseClient);
+}
+
+/// Obtains oauth2 credentials and returns an authenticated HTTP client.
+///
+/// See [obtainAccessCredentialsViaUserConsent] for specifics about the
+/// arguments used for obtaining access credentials.
+///
+/// Once access credentials have been obtained, this function will complete
+/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire
+/// it will use it's refresh token (if available) to obtain new credentials.
+/// See [autoRefreshingClient] for more information.
+///
+/// If [baseClient] is not given, one will be automatically created. It will be
+/// used for making authenticated HTTP requests.
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+Future<AutoRefreshingAuthClient> clientViaUserConsent(
+ ClientId clientId, List<String> scopes, PromptUserForConsent userPrompt,
+ {Client? baseClient}) async {
+ var closeUnderlyingClient = false;
+ if (baseClient == null) {
+ baseClient = Client();
+ closeUnderlyingClient = true;
+ }
+
+ final flow = AuthorizationCodeGrantServerFlow(
+ clientId, scopes, baseClient, userPrompt);
+
+ AccessCredentials credentials;
+
+ try {
+ credentials = await flow.run();
+ } catch (e) {
+ if (closeUnderlyingClient) {
+ baseClient.close();
+ }
+ rethrow;
+ }
+ return AutoRefreshingClient(baseClient, clientId, credentials,
+ closeUnderlyingClient: closeUnderlyingClient);
+}
+
+/// Obtains oauth2 credentials and returns an authenticated HTTP client.
+///
+/// See [obtainAccessCredentialsViaUserConsentManual] for specifics about the
+/// arguments used for obtaining access credentials.
+///
+/// Once access credentials have been obtained, this function will complete
+/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire
+/// it will use it's refresh token (if available) to obtain new credentials.
+/// See [autoRefreshingClient] for more information.
+///
+/// If [baseClient] is not given, one will be automatically created. It will be
+/// used for making authenticated HTTP requests.
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+Future<AutoRefreshingAuthClient> clientViaUserConsentManual(ClientId clientId,
+ List<String> scopes, PromptUserForConsentManual userPrompt,
+ {Client? baseClient}) async {
+ var closeUnderlyingClient = false;
+ if (baseClient == null) {
+ baseClient = Client();
+ closeUnderlyingClient = true;
+ }
+
+ final flow = AuthorizationCodeGrantManualFlow(
+ clientId, scopes, baseClient, userPrompt);
+
+ AccessCredentials credentials;
+
+ try {
+ credentials = await flow.run();
+ } catch (e) {
+ if (closeUnderlyingClient) {
+ baseClient.close();
+ }
+ rethrow;
+ }
+
+ return AutoRefreshingClient(baseClient, clientId, credentials,
+ closeUnderlyingClient: closeUnderlyingClient);
+}
+
+/// Obtains oauth2 credentials and returns an authenticated HTTP client.
+///
+/// See [obtainAccessCredentialsViaServiceAccount] for specifics about the
+/// arguments used for obtaining access credentials.
+///
+/// Once access credentials have been obtained, this function will complete
+/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire
+/// it will obtain new access credentials.
+///
+/// If [baseClient] is not given, one will be automatically created. It will be
+/// used for making authenticated HTTP requests and for obtaining access
+/// credentials.
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+Future<AutoRefreshingAuthClient> clientViaServiceAccount(
+ ServiceAccountCredentials clientCredentials, List<String> scopes,
+ {Client? baseClient}) async {
+ if (baseClient == null) {
+ baseClient = Client();
+ } else {
+ baseClient = nonClosingClient(baseClient);
+ }
+
+ final flow = JwtFlow(clientCredentials.email, clientCredentials.privateRSAKey,
+ clientCredentials.impersonatedUser, scopes, baseClient);
+
+ AccessCredentials credentials;
+ try {
+ credentials = await flow.run();
+ } catch (e) {
+ baseClient.close();
+ rethrow;
+ }
+
+ return _ServiceAccountClient(baseClient, credentials, flow);
+}
+
+/// Obtains oauth2 credentials and returns an authenticated HTTP client.
+///
+/// See [obtainAccessCredentialsViaMetadataServer] for specifics about the
+/// arguments used for obtaining access credentials.
+///
+/// Once access credentials have been obtained, this function will complete
+/// with an auto-refreshing HTTP client. Once the `AccessCredentials` expire
+/// it will obtain new access credentials.
+///
+/// If [baseClient] is not given, one will be automatically created. It will be
+/// used for making authenticated HTTP requests and for obtaining access
+/// credentials.
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+Future<AutoRefreshingAuthClient> clientViaMetadataServer(
+ {Client? baseClient}) async {
+ if (baseClient == null) {
+ baseClient = Client();
+ } else {
+ baseClient = nonClosingClient(baseClient);
+ }
+
+ final flow = MetadataServerAuthorizationFlow(baseClient);
+
+ AccessCredentials credentials;
+
+ try {
+ credentials = await flow.run();
+ } catch (e) {
+ baseClient.close();
+ rethrow;
+ }
+ return _MetadataServerClient(baseClient, credentials, flow);
+}
+
+/// Obtains a HTTP client which uses the given [apiKey] for making HTTP
+/// requests.
+///
+/// Note that the returned client should *only* be used for making HTTP requests
+/// to Google Services. The [apiKey] should not be disclosed to third parties.
+///
+/// The user is responsible for closing the returned HTTP [Client].
+/// Closing the returned [Client] will not close [baseClient].
+Client clientViaApiKey(String apiKey, {Client? baseClient}) {
+ if (baseClient == null) {
+ baseClient = Client();
+ } else {
+ baseClient = nonClosingClient(baseClient);
+ }
+ return ApiKeyClient(baseClient, apiKey);
+}
+
+/// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow.
+///
+/// The returned future will complete with `AccessCredentials` if the user
+/// has given the application access to it's data. Otherwise the future will
+/// complete with a `UserConsentException`.
+///
+/// In case another error occurs the returned future will complete with an
+/// `Exception`.
+///
+/// [userPrompt] will be used for directing the user/user-agent to a URI. See
+/// [PromptUserForConsent] for more information.
+///
+/// [client] will be used for obtaining `AccessCredentials` from an
+/// authorization code.
+///
+/// The [ClientId] can be obtained in the Google Cloud Console.
+Future<AccessCredentials> obtainAccessCredentialsViaUserConsent(
+ ClientId clientId,
+ List<String> scopes,
+ Client client,
+ PromptUserForConsent userPrompt) =>
+ AuthorizationCodeGrantServerFlow(clientId, scopes, client, userPrompt)
+ .run();
+
+/// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow.
+///
+/// The returned future will complete with `AccessCredentials` if the user
+/// has given the application access to it's data. Otherwise the future will
+/// complete with a `UserConsentException`.
+///
+/// In case another error occurs the returned future will complete with an
+/// `Exception`.
+///
+/// [userPrompt] will be used for directing the user/user-agent to a URI. See
+/// [PromptUserForConsentManual] for more information.
+///
+/// [client] will be used for obtaining `AccessCredentials` from an
+/// authorization code.
+///
+/// The [ClientId] can be obtained in the Google Cloud Console.
+Future<AccessCredentials> obtainAccessCredentialsViaUserConsentManual(
+ ClientId clientId,
+ List<String> scopes,
+ Client client,
+ PromptUserForConsentManual userPrompt) =>
+ AuthorizationCodeGrantManualFlow(clientId, scopes, client, userPrompt)
+ .run();
+
+/// Obtain oauth2 [AccessCredentials] using service account credentials.
+///
+/// In case the service account has no access to the requested scopes or another
+/// error occurs the returned future will complete with an `Exception`.
+///
+/// [baseClient] will be used for obtaining `AccessCredentials`.
+///
+/// The [ServiceAccountCredentials] can be obtained in the Google Cloud Console.
+Future<AccessCredentials> obtainAccessCredentialsViaServiceAccount(
+ ServiceAccountCredentials clientCredentials,
+ List<String> scopes,
+ Client baseClient) =>
+ JwtFlow(clientCredentials.email, clientCredentials.privateRSAKey,
+ clientCredentials.impersonatedUser, scopes, baseClient)
+ .run();
+
+/// Obtain oauth2 [AccessCredentials] using the metadata API on ComputeEngine.
+///
+/// In case the VM was not configured with access to the requested scopes or an
+/// error occurs the returned future will complete with an `Exception`.
+///
+/// [baseClient] will be used for obtaining `AccessCredentials`.
+///
+/// No credentials are needed. But this function is only intended to work on a
+/// Google Compute Engine VM with configured access to Google APIs.
+Future<AccessCredentials> obtainAccessCredentialsViaMetadataServer(
+ Client baseClient) =>
+ MetadataServerAuthorizationFlow(baseClient).run();
+
+/// Obtain oauth2 [AccessCredentials] by exchanging an authorization code.
+///
+/// Running a hybrid oauth2 flow as described in the
+/// `googleapis_auth.auth_browser` library results in a `HybridFlowResult` which
+/// contains short-lived [AccessCredentials] for the client and an authorization
+/// code. This authorization code needs to be transferred to the server, which
+/// can exchange it against long-lived [AccessCredentials].
+///
+/// If the authorization code was obtained using the mentioned hybrid flow, the
+/// [redirectUrl] must be `"postmessage"` (default).
+///
+/// If you obtained the authorization code using a different mechanism, the
+/// [redirectUrl] must be the same that was used to obtain the code.
+///
+/// NOTE: Only the server application will know the `client secret` - which is
+/// necessary to exchange an authorization code against access tokens.
+///
+/// NOTE: It is important to transmit the authorization code in a secure manner
+/// to the server. You should use "anti-request forgery state tokens" to guard
+/// against "cross site request forgery" attacks.
+/// An example on how to do this can be found here:
+/// https://developers.google.com/+/web/signin/server-side-flow
+Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange(
+ Client baseClient, ClientId clientId, String code,
+ {String redirectUrl = 'postmessage'}) =>
+ obtainAccessCredentialsUsingCode(clientId, code, redirectUrl, baseClient);
+
+/// Will close the underlying `http.Client`.
+class _ServiceAccountClient extends AutoRefreshDelegatingClient {
+ final JwtFlow flow;
+ @override
+ AccessCredentials credentials;
+ late Client authClient;
+
+ _ServiceAccountClient(Client client, this.credentials, this.flow)
+ : super(client) {
+ authClient = authenticatedClient(baseClient, credentials);
+ }
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) async {
+ if (!credentials.accessToken.hasExpired) {
+ return authClient.send(request);
+ } else {
+ final newCredentials = await flow.run();
+ notifyAboutNewCredentials(newCredentials);
+ credentials = newCredentials;
+ authClient = authenticatedClient(baseClient, credentials);
+ return authClient.send(request);
+ }
+ }
+}
+
+/// Will close the underlying `http.Client`.
+class _MetadataServerClient extends AutoRefreshDelegatingClient {
+ final MetadataServerAuthorizationFlow flow;
+ @override
+ AccessCredentials credentials;
+ Client authClient;
+
+ _MetadataServerClient(Client client, this.credentials, this.flow)
+ : authClient = authenticatedClient(client, credentials),
+ super(client);
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) async {
+ if (!credentials.accessToken.hasExpired) {
+ return authClient.send(request);
+ }
+
+ final newCredentials = await flow.run();
+ notifyAboutNewCredentials(newCredentials);
+ credentials = newCredentials;
+ authClient = authenticatedClient(baseClient, credentials);
+ return authClient.send(request);
+ }
+}
diff --git a/googleapis_auth/lib/src/adc_utils.dart b/googleapis_auth/lib/src/adc_utils.dart
new file mode 100644
index 0000000..d88240f
--- /dev/null
+++ b/googleapis_auth/lib/src/adc_utils.dart
@@ -0,0 +1,55 @@
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:http/http.dart';
+
+import '../auth_io.dart';
+import 'auth_http_utils.dart';
+
+Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
+ File file,
+ String fileSource,
+ List<String> scopes,
+ Client baseClient,
+) async {
+ Object? credentials;
+ try {
+ credentials = json.decode(await file.readAsString());
+ } on IOException {
+ throw Exception(
+ 'Failed to read credentials file from $fileSource',
+ );
+ } on FormatException {
+ throw Exception(
+ 'Failed to parse JSON from credentials file from $fileSource',
+ );
+ }
+
+ if (credentials is Map && credentials['type'] == 'authorized_user') {
+ final clientId = ClientId(
+ credentials['client_id'],
+ credentials['client_secret'],
+ );
+ return AutoRefreshingClient(
+ baseClient,
+ clientId,
+ await refreshCredentials(
+ clientId,
+ AccessCredentials(
+ // Hack: Create empty credentials that have expired.
+ AccessToken('Bearer', '', DateTime(0).toUtc()),
+ credentials['refresh_token'],
+ scopes,
+ ),
+ baseClient,
+ ),
+ quotaProject: credentials['quota_project_id'],
+ );
+ }
+ return await clientViaServiceAccount(
+ ServiceAccountCredentials.fromJson(credentials),
+ scopes,
+ baseClient: baseClient,
+ );
+}
diff --git a/googleapis_auth/lib/src/auth_http_utils.dart b/googleapis_auth/lib/src/auth_http_utils.dart
new file mode 100644
index 0000000..d327160
--- /dev/null
+++ b/googleapis_auth/lib/src/auth_http_utils.dart
@@ -0,0 +1,145 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth;
+
+import 'dart:async';
+
+import 'package:http/http.dart';
+
+import '../auth.dart';
+import 'http_client_base.dart';
+
+/// Will close the underlying `http.Client` depending on a constructor argument.
+class AuthenticatedClient extends DelegatingClient implements AuthClient {
+ @override
+ final AccessCredentials credentials;
+ final String? quotaProject;
+
+ AuthenticatedClient(Client client, this.credentials, {this.quotaProject})
+ : super(client, closeUnderlyingClient: false);
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) async {
+ // Make new request object and perform the authenticated request.
+ final modifiedRequest =
+ RequestImpl(request.method, request.url, request.finalize());
+ modifiedRequest.headers.addAll(request.headers);
+ modifiedRequest.headers['Authorization'] =
+ 'Bearer ${credentials.accessToken.data}';
+ if (quotaProject != null) {
+ modifiedRequest.headers['X-Goog-User-Project'] = quotaProject!;
+ }
+ final response = await baseClient.send(modifiedRequest);
+ final wwwAuthenticate = response.headers['www-authenticate'];
+ if (wwwAuthenticate != null) {
+ await response.stream.drain();
+ throw AccessDeniedException('Access was denied '
+ '(www-authenticate header was: $wwwAuthenticate).');
+ }
+ return response;
+ }
+}
+
+/// Adds 'key' query parameter when making HTTP requests.
+///
+/// If 'key' is already present on the URI, it will complete with an exception.
+/// This will prevent accidental overrides of a query parameter with the API
+/// key.
+class ApiKeyClient extends DelegatingClient {
+ final String _encodedApiKey;
+
+ ApiKeyClient(Client client, String apiKey)
+ : _encodedApiKey = Uri.encodeQueryComponent(apiKey),
+ super(client, closeUnderlyingClient: true);
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) {
+ var url = request.url;
+ if (url.queryParameters.containsKey('key')) {
+ return Future.error(Exception(
+ 'Tried to make a HTTP request which has already a "key" query '
+ 'parameter. Adding the API key would override that existing value.'));
+ }
+
+ if (url.query == '') {
+ url = url.replace(query: 'key=$_encodedApiKey');
+ } else {
+ url = url.replace(query: '${url.query}&key=$_encodedApiKey');
+ }
+
+ final modifiedRequest =
+ RequestImpl(request.method, url, request.finalize());
+ modifiedRequest.headers.addAll(request.headers);
+ return baseClient.send(modifiedRequest);
+ }
+}
+
+/// Will close the underlying `http.Client` depending on a constructor argument.
+class AutoRefreshingClient extends AutoRefreshDelegatingClient {
+ final ClientId clientId;
+ final String? quotaProject;
+ @override
+ AccessCredentials credentials;
+ late Client authClient;
+
+ AutoRefreshingClient(
+ Client client,
+ this.clientId,
+ this.credentials, {
+ bool closeUnderlyingClient = false,
+ this.quotaProject,
+ }) : assert(credentials.accessToken.type == 'Bearer'),
+ assert(credentials.refreshToken != null),
+ super(client, closeUnderlyingClient: closeUnderlyingClient) {
+ authClient = AuthenticatedClient(
+ baseClient,
+ credentials,
+ quotaProject: quotaProject,
+ );
+ }
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) async {
+ if (!credentials.accessToken.hasExpired) {
+ // TODO: Can this return a "access token expired" message?
+ // If so, we should handle it.
+ return authClient.send(request);
+ } else {
+ final cred = await refreshCredentials(clientId, credentials, baseClient);
+ notifyAboutNewCredentials(cred);
+ credentials = cred;
+ authClient = AuthenticatedClient(
+ baseClient,
+ cred,
+ quotaProject: quotaProject,
+ );
+ return authClient.send(request);
+ }
+ }
+}
+
+abstract class AutoRefreshDelegatingClient extends DelegatingClient
+ implements AutoRefreshingAuthClient {
+ final StreamController<AccessCredentials> _credentialStreamController =
+ StreamController.broadcast(sync: true);
+
+ AutoRefreshDelegatingClient(Client client,
+ {bool closeUnderlyingClient = true})
+ : super(client, closeUnderlyingClient: closeUnderlyingClient);
+
+ @override
+ Stream<AccessCredentials> get credentialUpdates =>
+ _credentialStreamController.stream;
+
+ void notifyAboutNewCredentials(AccessCredentials credentials) {
+ _credentialStreamController.add(credentials);
+ }
+
+ @override
+ void close() {
+ _credentialStreamController.close();
+ super.close();
+ }
+}
diff --git a/googleapis_auth/lib/src/crypto/asn1.dart b/googleapis_auth/lib/src/crypto/asn1.dart
new file mode 100644
index 0000000..76eb4a8
--- /dev/null
+++ b/googleapis_auth/lib/src/crypto/asn1.dart
@@ -0,0 +1,141 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library appengine_auth.asn;
+
+import 'dart:typed_data';
+
+import 'rsa.dart';
+
+class ASN1Parser {
+ static const integerTag = 0x02;
+ static const octetStringTag = 0x04;
+ static const nullTag = 0x05;
+ static const objectIdTag = 0x06;
+ static const sequenceTag = 0x30;
+
+ static ASN1Object parse(Uint8List bytes) {
+ Never invalidFormat(String msg) {
+ throw ArgumentError('Invalid DER encoding: $msg');
+ }
+
+ final data = ByteData.view(bytes.buffer);
+ var offset = 0;
+ final end = bytes.length;
+
+ void checkNBytesAvailable(int n) {
+ if ((offset + n) > end) {
+ invalidFormat('Tried to read more bytes than available.');
+ }
+ }
+
+ List<int> readBytes(int n) {
+ checkNBytesAvailable(n);
+
+ final integerBytes = bytes.sublist(offset, offset + n);
+ offset += n;
+ return integerBytes;
+ }
+
+ int readEncodedLength() {
+ checkNBytesAvailable(1);
+
+ final lengthByte = data.getUint8(offset++);
+
+ // Short length encoding form: This byte is the length itself.
+ if (lengthByte < 0x80) {
+ return lengthByte;
+ }
+
+ // Long length encoding form:
+ // This byte has in bits 0..6 the number of bytes following which encode
+ // the length.
+ var countLengthBytes = lengthByte & 0x7f;
+ checkNBytesAvailable(countLengthBytes);
+
+ var length = 0;
+ while (countLengthBytes > 0) {
+ length = (length << 8) | data.getUint8(offset++);
+ countLengthBytes--;
+ }
+ return length;
+ }
+
+ void readNullBytes() {
+ checkNBytesAvailable(1);
+ final nullByte = data.getUint8(offset++);
+ if (nullByte != 0x00) {
+ invalidFormat('Null byte expect, but was: $nullByte.');
+ }
+ }
+
+ ASN1Object decodeObject() {
+ checkNBytesAvailable(1);
+ final tag = bytes[offset++];
+ switch (tag) {
+ case integerTag:
+ final size = readEncodedLength();
+ return ASN1Integer(RSAAlgorithm.bytes2BigInt(readBytes(size)));
+ case octetStringTag:
+ final size = readEncodedLength();
+ return ASN1OctetString(readBytes(size));
+ case nullTag:
+ readNullBytes();
+ return ASN1Null();
+ case objectIdTag:
+ final size = readEncodedLength();
+ return ASN1ObjectIdentifier(readBytes(size));
+ case sequenceTag:
+ final lengthInBytes = readEncodedLength();
+ if ((offset + lengthInBytes) > end) {
+ invalidFormat('Tried to read more bytes than available.');
+ }
+ final endOfSequence = offset + lengthInBytes;
+
+ final objects = <ASN1Object>[];
+ while (offset < endOfSequence) {
+ objects.add(decodeObject());
+ }
+ return ASN1Sequence(objects);
+ default:
+ invalidFormat(
+ 'Unexpected tag $tag at offset ${offset - 1} (end: $end).');
+ }
+ }
+
+ final obj = decodeObject();
+ if (offset != bytes.length) {
+ throw ArgumentError('More bytes than expected in ASN1 encoding.');
+ }
+ return obj;
+ }
+}
+
+abstract class ASN1Object {}
+
+class ASN1Sequence extends ASN1Object {
+ final List<ASN1Object> objects;
+
+ ASN1Sequence(this.objects);
+}
+
+class ASN1Integer extends ASN1Object {
+ final BigInt integer;
+
+ ASN1Integer(this.integer);
+}
+
+class ASN1OctetString extends ASN1Object {
+ final List<int> bytes;
+
+ ASN1OctetString(this.bytes);
+}
+
+class ASN1ObjectIdentifier extends ASN1Object {
+ final List<int> bytes;
+
+ ASN1ObjectIdentifier(this.bytes);
+}
+
+class ASN1Null extends ASN1Object {}
diff --git a/googleapis_auth/lib/src/crypto/pem.dart b/googleapis_auth/lib/src/crypto/pem.dart
new file mode 100644
index 0000000..a01ba5e
--- /dev/null
+++ b/googleapis_auth/lib/src/crypto/pem.dart
@@ -0,0 +1,214 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.pem;
+
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'asn1.dart';
+import 'rsa.dart';
+
+/// Decode a [RSAPrivateKey] from the string content of a PEM file.
+///
+/// A PEM file can be extracted from a .p12 cryptostore with
+/// $ openssl pkcs12 -nocerts -nodes -passin pass:notasecret \
+/// -in *-privatekey.p12 -out *-privatekey.pem
+RSAPrivateKey keyFromString(String pemFileString) {
+ final bytes = _getBytesFromPEMString(pemFileString);
+ return _extractRSAKeyFromDERBytes(bytes);
+}
+
+/// Helper function for decoding the base64 in [pemString].
+Uint8List _getBytesFromPEMString(String pemString) {
+ final lines = LineSplitter.split(pemString)
+ .map((line) => line.trim())
+ .where((line) => line.isNotEmpty)
+ .toList();
+
+ if (lines.length < 2 ||
+ !lines.first.startsWith('-----BEGIN') ||
+ !lines.last.startsWith('-----END')) {
+ throw ArgumentError('The given string does not have the correct '
+ 'begin/end markers expected in a PEM file.');
+ }
+ final base64 = lines.sublist(1, lines.length - 1).join();
+ return Uint8List.fromList(base64Decode(base64));
+}
+
+/// Helper to decode the ASN.1/DER bytes in [bytes] into an [RSAPrivateKey].
+RSAPrivateKey _extractRSAKeyFromDERBytes(Uint8List bytes) {
+ // We recognize two formats:
+ // Real format:
+ //
+ // PrivateKey := seq[int/version=0, int/n, int/e, int/d, int/p,
+ // int/q, int/dmp1, int/dmq1, int/coeff]
+ //
+ // Or the above `PrivateKey` embeddded inside another ASN object:
+ // Encapsulated := seq[int/version=0,
+ // seq[obj-id/rsa-id, null-obj],
+ // octet-string/PrivateKey]
+ //
+
+ RSAPrivateKey privateKeyFromSequence(ASN1Sequence asnSequence) {
+ final objects = asnSequence.objects;
+
+ final asnIntegers = objects.take(9).map((o) => o as ASN1Integer).toList();
+
+ final version = asnIntegers.first;
+ if (version.integer != BigInt.zero) {
+ throw ArgumentError('Expected version 0, got: ${version.integer}.');
+ }
+
+ final key = RSAPrivateKey(
+ asnIntegers[1].integer,
+ asnIntegers[2].integer,
+ asnIntegers[3].integer,
+ asnIntegers[4].integer,
+ asnIntegers[5].integer,
+ asnIntegers[6].integer,
+ asnIntegers[7].integer,
+ asnIntegers[8].integer);
+
+ final bitLength = key.bitLength;
+ if (bitLength != 1024 && bitLength != 2048 && bitLength != 4096) {
+ throw ArgumentError('The RSA modulus has a bit length of $bitLength. '
+ 'Only 1024, 2048 and 4096 are supported.');
+ }
+ return key;
+ }
+
+ try {
+ final asn = ASN1Parser.parse(bytes);
+ if (asn is ASN1Sequence) {
+ final objects = asn.objects;
+ if (objects.length == 3 && objects[2] is ASN1OctetString) {
+ final string = objects[2] as ASN1OctetString;
+ // Seems like the embedded form.
+ // TODO: Validate that rsa identifier matches!
+ return privateKeyFromSequence(
+ ASN1Parser.parse(string.bytes as Uint8List) as ASN1Sequence);
+ }
+ }
+ return privateKeyFromSequence(asn as ASN1Sequence);
+ } catch (error) {
+ throw ArgumentError(
+ 'Error while extracting private key from DER bytes: $error');
+ }
+}
+
+/*
+ * Example of generating a public/private RSA keypair with 2048 bits and dumping
+ * the structure of the resulting private key.
+
+ $ openssl genrsa -out key.pem 2048
+ Generating RSA private key, 2048 bit long modulus
+ ..................................................+++
+ ..................................................+++
+ e is 65537 (0x10001)
+
+ $ cat key.pem
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEowIBAAKCAQEAuDOwXO14ltE1j2O0iDSuqtbw/1kMKjeiki3oehk2zNoUte42
+ /s2rX15nYCkKtYG/r8WYvKzb31P4Uow1S4fFydKNWxgX4VtEjHgeqfPxeCL9wiJc
+ 9KkEt4fyhj1Jo7193gCLtovLAFwPzAMbFLiXWkfqalJ5Z77fOE4Mo7u4pEgxNPgL
+ VFGe0cEOAsHsKlsze+m1pmPHwWNVTcoKe5o0hOzy6hCPgVc6me6Y7aO8Fb4OVg0l
+ XQdQpWn2ikVBpzBcZ6InnYyJ/CJNa3WL1LJ65mmYnfHtKGoMqhLK48OReguwRwwF
+ e9/2+8UcdZcN5rsvt7yg3ZrKNH8rx+wZ36sRewIDAQABAoIBAQCn1HCcOsHkqDlk
+ rDOQ5m8+uRhbj4bF8GrvRWTL2q1TeF/mY2U4Q6wg+KK3uq1HMzCzthWz0suCb7+R
+ dq4YY1ySxoSEuy8G5WFPmyJVNy6Lh1Yty6FmSZlCn1sZdD3kMoK8A0NIz5Xmffrm
+ pu3Fs2ozl9K9jOeQ3xgC9RoPFLrm8lHJ45Vn+SnTxZnsXT6pwpg3TnFIx5ZinU8k
+ l0Um1n80qD2QQDakQ5jyr2odAELLBDlyCkxAglBXAVt4nk9Kl6nxb4snd9dnrL70
+ WjLynWQsDczaV9TZIl2hYkMud+9OLVlUUtB+0c5b0p2t2P0sLltDaq3H6pT6yu2G
+ 8E86J9IBAoGBAPJaTNV5ysVOFn+YwWwRztzrvNArUJkVq8abN0gGp3gUvDEZnvzK
+ weF7+lfZzcwVRmQkL3mWLzzZvCx77RfulAzLi5iFuRBPhhhxAPDiDuyL9B7O81G/
+ M/W5DPctGOyD/9cnLuh72oij0unc5MLSfzJf8wblpcjJnPBDqIVh6Qt9AoGBAMKT
+ Gacf4iSj1xW+0wrnbZlDuyCl6Msptj8ePcvLQrFqQmBwsXmWgVR+gFc/1G3lRft0
+ QC6chsmafQHIIPpaDjq3sQ01/tUu7LXL+g/Hw9XtUHbkg3sZIQBtC26rKdStfHNS
+ KTvuCgn/dAJNjiohfhWMt9R4Q6E5FV6PqQHJzPJXAoGAC41qZDKuC8GxKNvrPG+M
+ 4NML6RBngySZT5pOhExs5zh10BFclshDfbAfOtjTCotpE5T1/mG+VrQ6WBSANMfW
+ ntWFDfwx2ikwRzH7zX+5HmV9eYp75sWqgGgVyiKIMZ4JMARaJBLjU+gbQbKZ5P+L
+ uKcCOq3vvSZ/KKTQ/6qvJTECgYBiWgbCgoxF5wdmd4Gn5llw+lqRYyur3hbACuJD
+ rCe3FDYfF3euNRSEiDkJYTtYnWbldtqmdPpw14VOrEF3KqQ8q/Nz8RIx4jlGn6dz
+ 6I8mCIH+xv1q8MXMuFHqC9zmIxdgF2y+XVF3wkd6jodI5oscC3g0juHokbkqhkVw
+ oPfWmwKBgBfR6jv0gWWeWTfkNwj+cMLHQV1uvz6JyLH5K4iISEDFxYkd37jrHB8A
+ 9hz9UDfmCbSs2j8CXDg7zCayM6tfu4Vtx+8S5g3oN6sa1JXFY1Os7SoXhTfX9M+7
+ QpYYDJZwkgZrVQoKMIdCs9xfyVhZERq945NYLekwE1t2W+tOVBgR
+ -----END RSA PRIVATE KEY-----
+
+ $ openssl enc -d -base64 -in key.pem -out key.bin
+
+ $ dumpasn1 key.bin
+ 0 1187: SEQUENCE {
+ 4 1: INTEGER 0
+ 7 257: INTEGER
+ : 00 B8 33 B0 5C ED 78 96 D1 35 8F 63 B4 88 34 AE
+ : AA D6 F0 FF 59 0C 2A 37 A2 92 2D E8 7A 19 36 CC
+ : DA 14 B5 EE 36 FE CD AB 5F 5E 67 60 29 0A B5 81
+ : BF AF C5 98 BC AC DB DF 53 F8 52 8C 35 4B 87 C5
+ : C9 D2 8D 5B 18 17 E1 5B 44 8C 78 1E A9 F3 F1 78
+ : 22 FD C2 22 5C F4 A9 04 B7 87 F2 86 3D 49 A3 BD
+ : 7D DE 00 8B B6 8B CB 00 5C 0F CC 03 1B 14 B8 97
+ : 5A 47 EA 6A 52 79 67 BE DF 38 4E 0C A3 BB B8 A4
+ : [ Another 129 bytes skipped ]
+ 268 3: INTEGER 65537
+ 273 257: INTEGER
+ : 00 A7 D4 70 9C 3A C1 E4 A8 39 64 AC 33 90 E6 6F
+ : 3E B9 18 5B 8F 86 C5 F0 6A EF 45 64 CB DA AD 53
+ : 78 5F E6 63 65 38 43 AC 20 F8 A2 B7 BA AD 47 33
+ : 30 B3 B6 15 B3 D2 CB 82 6F BF 91 76 AE 18 63 5C
+ : 92 C6 84 84 BB 2F 06 E5 61 4F 9B 22 55 37 2E 8B
+ : 87 56 2D CB A1 66 49 99 42 9F 5B 19 74 3D E4 32
+ : 82 BC 03 43 48 CF 95 E6 7D FA E6 A6 ED C5 B3 6A
+ : 33 97 D2 BD 8C E7 90 DF 18 02 F5 1A 0F 14 BA E6
+ : [ Another 129 bytes skipped ]
+ 534 129: INTEGER
+ : 00 F2 5A 4C D5 79 CA C5 4E 16 7F 98 C1 6C 11 CE
+ : DC EB BC D0 2B 50 99 15 AB C6 9B 37 48 06 A7 78
+ : 14 BC 31 19 9E FC CA C1 E1 7B FA 57 D9 CD CC 15
+ : 46 64 24 2F 79 96 2F 3C D9 BC 2C 7B ED 17 EE 94
+ : 0C CB 8B 98 85 B9 10 4F 86 18 71 00 F0 E2 0E EC
+ : 8B F4 1E CE F3 51 BF 33 F5 B9 0C F7 2D 18 EC 83
+ : FF D7 27 2E E8 7B DA 88 A3 D2 E9 DC E4 C2 D2 7F
+ : 32 5F F3 06 E5 A5 C8 C9 9C F0 43 A8 85 61 E9 0B
+ : [ Another 1 bytes skipped ]
+ 666 129: INTEGER
+ : 00 C2 93 19 A7 1F E2 24 A3 D7 15 BE D3 0A E7 6D
+ : 99 43 BB 20 A5 E8 CB 29 B6 3F 1E 3D CB CB 42 B1
+ : 6A 42 60 70 B1 79 96 81 54 7E 80 57 3F D4 6D E5
+ : 45 FB 74 40 2E 9C 86 C9 9A 7D 01 C8 20 FA 5A 0E
+ : 3A B7 B1 0D 35 FE D5 2E EC B5 CB FA 0F C7 C3 D5
+ : ED 50 76 E4 83 7B 19 21 00 6D 0B 6E AB 29 D4 AD
+ : 7C 73 52 29 3B EE 0A 09 FF 74 02 4D 8E 2A 21 7E
+ : 15 8C B7 D4 78 43 A1 39 15 5E 8F A9 01 C9 CC F2
+ : [ Another 1 bytes skipped ]
+ 798 128: INTEGER
+ : 0B 8D 6A 64 32 AE 0B C1 B1 28 DB EB 3C 6F 8C E0
+ : D3 0B E9 10 67 83 24 99 4F 9A 4E 84 4C 6C E7 38
+ : 75 D0 11 5C 96 C8 43 7D B0 1F 3A D8 D3 0A 8B 69
+ : 13 94 F5 FE 61 BE 56 B4 3A 58 14 80 34 C7 D6 9E
+ : D5 85 0D FC 31 DA 29 30 47 31 FB CD 7F B9 1E 65
+ : 7D 79 8A 7B E6 C5 AA 80 68 15 CA 22 88 31 9E 09
+ : 30 04 5A 24 12 E3 53 E8 1B 41 B2 99 E4 FF 8B B8
+ : A7 02 3A AD EF BD 26 7F 28 A4 D0 FF AA AF 25 31
+ 929 128: INTEGER
+ : 62 5A 06 C2 82 8C 45 E7 07 66 77 81 A7 E6 59 70
+ : FA 5A 91 63 2B AB DE 16 C0 0A E2 43 AC 27 B7 14
+ : 36 1F 17 77 AE 35 14 84 88 39 09 61 3B 58 9D 66
+ : E5 76 DA A6 74 FA 70 D7 85 4E AC 41 77 2A A4 3C
+ : AB F3 73 F1 12 31 E2 39 46 9F A7 73 E8 8F 26 08
+ : 81 FE C6 FD 6A F0 C5 CC B8 51 EA 0B DC E6 23 17
+ : 60 17 6C BE 5D 51 77 C2 47 7A 8E 87 48 E6 8B 1C
+ : 0B 78 34 8E E1 E8 91 B9 2A 86 45 70 A0 F7 D6 9B
+ 1060 128: INTEGER
+ : 17 D1 EA 3B F4 81 65 9E 59 37 E4 37 08 FE 70 C2
+ : C7 41 5D 6E BF 3E 89 C8 B1 F9 2B 88 88 48 40 C5
+ : C5 89 1D DF B8 EB 1C 1F 00 F6 1C FD 50 37 E6 09
+ : B4 AC DA 3F 02 5C 38 3B CC 26 B2 33 AB 5F BB 85
+ : 6D C7 EF 12 E6 0D E8 37 AB 1A D4 95 C5 63 53 AC
+ : ED 2A 17 85 37 D7 F4 CF BB 42 96 18 0C 96 70 92
+ : 06 6B 55 0A 0A 30 87 42 B3 DC 5F C9 58 59 11 1A
+ : BD E3 93 58 2D E9 30 13 5B 76 5B EB 4E 54 18 11
+ : }
+ */
diff --git a/googleapis_auth/lib/src/crypto/rsa.dart b/googleapis_auth/lib/src/crypto/rsa.dart
new file mode 100644
index 0000000..8c57660
--- /dev/null
+++ b/googleapis_auth/lib/src/crypto/rsa.dart
@@ -0,0 +1,111 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// A small part is based on a JavaScript implementation of RSA by Tom Wu
+// but re-written in dart.
+
+library googleapis_auth.rsa;
+
+import 'dart:typed_data';
+
+/// Represents integers obtained while creating a Public/Private key pair.
+class RSAPrivateKey {
+ /// First prime number.
+ final BigInt p;
+
+ /// Second prime number.
+ final BigInt q;
+
+ /// Modulus for public and private keys. Satisfies `n=p*q`.
+ final BigInt n;
+
+ /// Public key exponent. Satisfies `d*e=1 mod phi(n)`.
+ final BigInt e;
+
+ /// Private key exponent. Satisfies `d*e=1 mod phi(n)`.
+ final BigInt d;
+
+ /// Different form of [p]. Satisfies `dmp1=d mod (p-1)`.
+ final BigInt dmp1;
+
+ /// Different form of [p]. Satisfies `dmq1=d mod (q-1)`.
+ final BigInt dmq1;
+
+ /// A coefficient which satisfies `coeff=q^-1 mod p`.
+ final BigInt coeff;
+
+ /// The number of bits used for the modulus. Usually 1024, 2048 or 4096 bits.
+ int get bitLength => n.bitLength;
+
+ RSAPrivateKey(
+ this.n, this.e, this.d, this.p, this.q, this.dmp1, this.dmq1, this.coeff);
+}
+
+/// Provides a [encrypt] method for encrypting messages with a [RSAPrivateKey].
+abstract class RSAAlgorithm {
+ /// Performs the encryption of [bytes] with the private [key].
+ /// Others who have access to the public key will be able to decrypt this
+ /// the result.
+ ///
+ /// The [intendedLength] argument specifies the number of bytes in which the
+ /// result should be encoded. Zero bytes will be used for padding.
+ static List<int> encrypt(
+ RSAPrivateKey key, List<int> bytes, int intendedLength) {
+ final message = bytes2BigInt(bytes);
+ final encryptedMessage = _encryptInteger(key, message);
+ return integer2Bytes(encryptedMessage, intendedLength);
+ }
+
+ static BigInt _encryptInteger(RSAPrivateKey key, BigInt x) {
+ // The following is equivalent to `_modPow(x, key.d, key.n) but is much
+ // more efficient. It exploits the fact that we have dmp1/dmq1.
+ var xp = _modPow(x % key.p, key.dmp1, key.p);
+ final xq = _modPow(x % key.q, key.dmq1, key.q);
+ while (xp < xq) {
+ xp += key.p;
+ }
+ return ((((xp - xq) * key.coeff) % key.p) * key.q) + xq;
+ }
+
+ // TODO(kevmoo): see if this can be done more efficiently with BigInt
+ static BigInt _modPow(BigInt b, BigInt e, BigInt m) {
+ if (e < BigInt.one) {
+ return BigInt.one;
+ }
+ if (b < BigInt.zero || b > m) {
+ b = b % m;
+ }
+ var r = BigInt.one;
+ while (e > BigInt.zero) {
+ if ((e & BigInt.one) > BigInt.zero) {
+ r = (r * b) % m;
+ }
+ e >>= 1;
+ b = (b * b) % m;
+ }
+ return r;
+ }
+
+ static BigInt bytes2BigInt(List<int> bytes) {
+ var number = BigInt.zero;
+ for (var i = 0; i < bytes.length; i++) {
+ number = (number << 8) | BigInt.from(bytes[i]);
+ }
+ return number;
+ }
+
+ static List<int> integer2Bytes(BigInt integer, int intendedLength) {
+ if (integer < BigInt.one) {
+ throw ArgumentError('Only positive integers are supported.');
+ }
+ final bytes = Uint8List(intendedLength);
+ for (var i = bytes.length - 1; i >= 0; i--) {
+ bytes[i] = (integer & _bigIntFF).toInt();
+ integer >>= 8;
+ }
+ return bytes;
+ }
+}
+
+final _bigIntFF = BigInt.from(0xff);
diff --git a/googleapis_auth/lib/src/crypto/rsa_sign.dart b/googleapis_auth/lib/src/crypto/rsa_sign.dart
new file mode 100644
index 0000000..842bc78
--- /dev/null
+++ b/googleapis_auth/lib/src/crypto/rsa_sign.dart
@@ -0,0 +1,81 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.rsa_sign;
+
+import 'dart:typed_data';
+
+import 'package:crypto/crypto.dart';
+
+import 'asn1.dart';
+import 'rsa.dart';
+
+/// Used for signing messages with a private RSA key.
+///
+/// The implemented algorithm can be seen in
+/// RFC 3447, Section 9.2 EMSA-PKCS1-v1_5.
+class RS256Signer {
+ // NIST sha-256 OID (2 16 840 1 101 3 4 2 1)
+ // See a reference for the encoding here:
+ // http://msdn.microsoft.com/en-us/library/bb540809%28v=vs.85%29.aspx
+ static const _rsaSha256AlgorithmIdentifier = [
+ 0x06,
+ 0x09,
+ 0x60,
+ 0x86,
+ 0x48,
+ 0x01,
+ 0x65,
+ 0x03,
+ 0x04,
+ 0x02,
+ 0x01
+ ];
+
+ final RSAPrivateKey _rsaKey;
+
+ RS256Signer(this._rsaKey);
+
+ List<int> sign(List<int> bytes) {
+ final digest = _digestInfo(sha256.convert(bytes).bytes);
+ final modulusLen = (_rsaKey.bitLength + 7) ~/ 8;
+
+ final block = Uint8List(modulusLen);
+ final padLength = block.length - digest.length - 3;
+ block[0] = 0x00;
+ block[1] = 0x01;
+ block.fillRange(2, 2 + padLength, 0xFF);
+ block[2 + padLength] = 0x00;
+ block.setRange(2 + padLength + 1, block.length, digest);
+ return RSAAlgorithm.encrypt(_rsaKey, block, modulusLen);
+ }
+
+ static Uint8List _digestInfo(List<int> hash) {
+ // DigestInfo :== SEQUENCE {
+ // digestAlgorithm AlgorithmIdentifier,
+ // digest OCTET STRING
+ // }
+ var offset = 0;
+ final digestInfo = Uint8List(
+ 2 + 2 + _rsaSha256AlgorithmIdentifier.length + 2 + 2 + hash.length);
+ {
+ // DigestInfo
+ digestInfo[offset++] = ASN1Parser.sequenceTag;
+ digestInfo[offset++] = digestInfo.length - 2;
+ {
+ // AlgorithmIdentifier.
+ digestInfo[offset++] = ASN1Parser.sequenceTag;
+ digestInfo[offset++] = _rsaSha256AlgorithmIdentifier.length + 2;
+ digestInfo.setAll(offset, _rsaSha256AlgorithmIdentifier);
+ offset += _rsaSha256AlgorithmIdentifier.length;
+ digestInfo[offset++] = ASN1Parser.nullTag;
+ digestInfo[offset++] = 0;
+ }
+ digestInfo[offset++] = ASN1Parser.octetStringTag;
+ digestInfo[offset++] = hash.length;
+ digestInfo.setAll(offset, hash);
+ }
+ return digestInfo;
+ }
+}
diff --git a/googleapis_auth/lib/src/http_client_base.dart b/googleapis_auth/lib/src/http_client_base.dart
new file mode 100644
index 0000000..60a9f3f
--- /dev/null
+++ b/googleapis_auth/lib/src/http_client_base.dart
@@ -0,0 +1,105 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.http_client_base;
+
+import 'dart:async';
+
+import 'package:http/http.dart';
+
+/// Base class for delegating HTTP clients.
+///
+/// Depending on [closeUnderlyingClient] it will close the client it is
+/// delegating to or not.
+abstract class DelegatingClient extends BaseClient {
+ final Client baseClient;
+ final bool closeUnderlyingClient;
+ bool _isClosed = false;
+
+ DelegatingClient(this.baseClient, {this.closeUnderlyingClient = true});
+
+ @override
+ void close() {
+ if (_isClosed) {
+ throw StateError('Cannot close a HTTP client more than once.');
+ }
+ _isClosed = true;
+ super.close();
+
+ if (closeUnderlyingClient) {
+ baseClient.close();
+ }
+ }
+}
+
+/// A reference counted HTTP client.
+///
+/// It uses a base [Client] which will be closed once the reference count
+/// reaches zero. The initial reference count is one, since the caller has a
+/// reference to the constructed instance.
+class RefCountedClient extends DelegatingClient {
+ int _refCount;
+
+ RefCountedClient(Client baseClient, {int initialRefCount = 1})
+ : _refCount = initialRefCount,
+ super(baseClient, closeUnderlyingClient: true);
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) {
+ _ensureClientIsOpen();
+ return baseClient.send(request);
+ }
+
+ /// Acquires a new reference which causes the reference count to be
+ /// incremented by 1.
+ void acquire() {
+ _ensureClientIsOpen();
+ _refCount++;
+ }
+
+ /// Releases a new reference which causes the reference count to be
+ /// decremented by 1.
+ void release() {
+ _ensureClientIsOpen();
+ _refCount--;
+
+ if (_refCount == 0) {
+ super.close();
+ }
+ }
+
+ /// Is equivalent to calling `release`.
+ @override
+ void close() {
+ release();
+ }
+
+ void _ensureClientIsOpen() {
+ if (_refCount <= 0) {
+ throw StateError(
+ 'This reference counted HTTP client has reached a count of zero and '
+ 'can no longer be used for making HTTP requests.');
+ }
+ }
+}
+
+// NOTE:
+// Calling close on the returned client once will not close the underlying
+// [baseClient].
+Client nonClosingClient(Client baseClient) =>
+ RefCountedClient(baseClient, initialRefCount: 2);
+
+class RequestImpl extends BaseRequest {
+ final Stream<List<int>> _stream;
+
+ RequestImpl(String method, Uri url, [Stream<List<int>>? stream])
+ : _stream = stream ?? const Stream.empty(),
+ super(method, url);
+
+ @override
+ ByteStream finalize() {
+ super.finalize();
+ return ByteStream(_stream);
+ }
+}
diff --git a/googleapis_auth/lib/src/oauth2_flows/auth_code.dart b/googleapis_auth/lib/src/oauth2_flows/auth_code.dart
new file mode 100644
index 0000000..e5a2b8c
--- /dev/null
+++ b/googleapis_auth/lib/src/oauth2_flows/auth_code.dart
@@ -0,0 +1,272 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.auth_code_flow;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+
+import '../../auth.dart';
+import '../http_client_base.dart';
+import '../typedefs.dart';
+import '../utils.dart';
+
+// The OAuth2 Token endpoint can be used to make requests as
+// https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=<token>
+//
+// A successfull response from the server will give an HTTP response status
+// 200 and a body of the following type:
+// {
+// "issued_to": "XYZ.apps.googleusercontent.com",
+// "audience": "XYZ.apps.googleusercontent.com",
+// "scope": "https://www.googleapis.com/auth/bigquery",
+// "expires_in": 3547,
+// "access_type": "offline"
+// }
+//
+// Scopes are separated by spaces.
+Future<List<String>> obtainScopesFromAccessToken(
+ String accessToken, http.Client client) async {
+ final url = Uri.parse('https://www.googleapis.com/oauth2/v2/tokeninfo'
+ '?access_token=${Uri.encodeQueryComponent(accessToken)}');
+
+ final response = await client.post(url);
+ if (response.statusCode == 200) {
+ final Map json = jsonDecode(response.body);
+ final scope = json['scope'];
+ if (scope is! String) {
+ throw Exception(
+ 'The response did not include a `scope` value of type `String`.');
+ }
+ return scope.split(' ').toList();
+ } else {
+ throw Exception('Unable to obtain list of scopes an access token '
+ 'is valid for. Server responded with ${response.statusCode}.');
+ }
+}
+
+Future<AccessCredentials> obtainAccessCredentialsUsingCode(
+ ClientId clientId, String code, String redirectUrl, http.Client client,
+ [List<String>? scopes]) async {
+ final uri = Uri.parse('https://accounts.google.com/o/oauth2/token');
+ final formValues = [
+ 'grant_type=authorization_code',
+ 'code=${Uri.encodeQueryComponent(code)}',
+ 'redirect_uri=${Uri.encodeQueryComponent(redirectUrl)}',
+ 'client_id=${Uri.encodeQueryComponent(clientId.identifier)}',
+ 'client_secret=${Uri.encodeQueryComponent(clientId.secret!)}',
+ ];
+
+ final body = Stream<List<int>>.fromIterable(
+ <List<int>>[ascii.encode(formValues.join('&'))]);
+ final request = RequestImpl('POST', uri, body);
+ request.headers['content-type'] = contentTypeUrlEncoded;
+
+ final response = await client.send(request);
+ final jsonMap = await utf8.decoder
+ .bind(response.stream)
+ .transform(json.decoder)
+ .first as Map;
+
+ final idToken = jsonMap['id_token'];
+ final tokenType = jsonMap['token_type'];
+ final accessToken = jsonMap['access_token'];
+ final seconds = jsonMap['expires_in'];
+ final refreshToken = jsonMap['refresh_token'];
+ final error = jsonMap['error'];
+
+ if (response.statusCode != 200 && error != null) {
+ throw Exception('Failed to exchange authorization code. '
+ 'Response was ${response.statusCode}. Error message was $error.');
+ }
+
+ if (response.statusCode != 200 ||
+ accessToken == null ||
+ seconds is! int ||
+ tokenType != 'Bearer') {
+ throw Exception('Failed to exchange authorization code. '
+ 'Invalid server response. '
+ 'Http status code was: ${response.statusCode}.');
+ }
+
+ if (scopes != null) {
+ return AccessCredentials(
+ AccessToken('Bearer', accessToken, expiryDate(seconds)),
+ refreshToken,
+ scopes,
+ idToken: idToken);
+ }
+
+ scopes = await obtainScopesFromAccessToken(accessToken, client);
+ return AccessCredentials(
+ AccessToken('Bearer', accessToken, expiryDate(seconds)),
+ refreshToken,
+ scopes,
+ idToken: idToken);
+}
+
+/// Abstract class for obtaining access credentials via the authorization code
+/// grant flow
+///
+/// See
+/// * [AuthorizationCodeGrantServerFlow]
+/// * [AuthorizationCodeGrantManualFlow]
+/// for further details.
+abstract class AuthorizationCodeGrantAbstractFlow {
+ final ClientId clientId;
+ final List<String> scopes;
+ final http.Client _client;
+
+ AuthorizationCodeGrantAbstractFlow(this.clientId, this.scopes, this._client);
+
+ Future<AccessCredentials> run();
+
+ Future<AccessCredentials> _obtainAccessCredentialsUsingCode(
+ String code, String redirectUri) =>
+ obtainAccessCredentialsUsingCode(
+ clientId, code, redirectUri, _client, scopes);
+
+ String _authenticationUri(String redirectUri, {String? state}) {
+ // TODO: Increase scopes with [include_granted_scopes].
+ final queryValues = [
+ 'response_type=code',
+ 'client_id=${Uri.encodeQueryComponent(clientId.identifier)}',
+ 'redirect_uri=${Uri.encodeQueryComponent(redirectUri)}',
+ 'scope=${Uri.encodeQueryComponent(scopes.join(' '))}',
+ ];
+ if (state != null) {
+ queryValues.add('state=${Uri.encodeQueryComponent(state)}');
+ }
+ return Uri.parse('https://accounts.google.com/o/oauth2/auth'
+ '?${queryValues.join('&')}')
+ .toString();
+ }
+}
+
+/// Runs an oauth2 authorization code grant flow using an HTTP server.
+///
+/// This class is able to run an oauth2 authorization flow. It takes a user
+/// supplied function which will be called with an URI. The user is expected
+/// to navigate to that URI and to grant access to the client.
+///
+/// Once the user has granted access to the client, Google will redirect the
+/// user agent to a URL pointing to a locally running HTTP server. Which in turn
+/// will be able to extract the authorization code from the URL and use it to
+/// obtain access credentials.
+class AuthorizationCodeGrantServerFlow
+ extends AuthorizationCodeGrantAbstractFlow {
+ final PromptUserForConsent userPrompt;
+
+ AuthorizationCodeGrantServerFlow(ClientId clientId, List<String> scopes,
+ http.Client client, this.userPrompt)
+ : super(clientId, scopes, client);
+
+ @override
+ Future<AccessCredentials> run() async {
+ final server = await HttpServer.bind('localhost', 0);
+
+ try {
+ final port = server.port;
+ final redirectionUri = 'http://localhost:$port';
+ final state = 'authcodestate${DateTime.now().millisecondsSinceEpoch}';
+
+ // Prompt user and wait until he goes to URL and the google authorization
+ // server calls back to our locally running HTTP server.
+ userPrompt(_authenticationUri(redirectionUri, state: state));
+
+ final request = await server.first;
+ final uri = request.uri;
+
+ try {
+ final returnedState = uri.queryParameters['state'];
+ final code = uri.queryParameters['code'];
+ final error = uri.queryParameters['error'];
+
+ if (request.method != 'GET') {
+ throw Exception('Invalid response from server '
+ '(expected GET request callback, got: ${request.method}).');
+ }
+
+ if (state != returnedState) {
+ throw Exception(
+ 'Invalid response from server (state did not match).');
+ }
+
+ if (error != null) {
+ throw UserConsentException(
+ 'Error occured while obtaining access credentials: $error');
+ }
+
+ if (code == null || code == '') {
+ throw Exception(
+ 'Invalid response from server (no auth code transmitted).');
+ }
+ final credentials =
+ await _obtainAccessCredentialsUsingCode(code, redirectionUri);
+
+ // TODO: We could introduce a user-defined redirect page.
+ request.response
+ ..statusCode = 200
+ ..headers.set('content-type', 'text/html; charset=UTF-8')
+ ..write('''
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Authorization successful.</title>
+ </head>
+
+ <body>
+ <h2 style="text-align: center">Application has successfully obtained access credentials</h2>
+ <p style="text-align: center">This window can be closed now.</p>
+ </body>
+</html>''');
+ await request.response.close();
+ return credentials;
+ } catch (e) {
+ request.response.statusCode = 500;
+ await request.response.close().catchError((_) {});
+ rethrow;
+ }
+ } finally {
+ await server.close();
+ }
+ }
+}
+
+/// Runs an oauth2 authorization code grant flow using manual Copy&Paste.
+///
+/// This class is able to run an oauth2 authorization flow. It takes a user
+/// supplied function which will be called with an URI. The user is expected
+/// to navigate to that URI and to grant access to the client.
+///
+/// Google will give the resource owner a code. The user supplied function needs
+/// to complete with that code.
+///
+/// The authorization code will then be used to obtain access credentials.
+class AuthorizationCodeGrantManualFlow
+ extends AuthorizationCodeGrantAbstractFlow {
+ final PromptUserForConsentManual userPrompt;
+
+ AuthorizationCodeGrantManualFlow(ClientId clientId, List<String> scopes,
+ http.Client client, this.userPrompt)
+ : super(clientId, scopes, client);
+
+ @override
+ Future<AccessCredentials> run() async {
+ const redirectionUri = 'urn:ietf:wg:oauth:2.0:oob';
+
+ // Prompt user and wait until he goes to URL and copy&pastes the auth code
+ // in.
+ final code = await userPrompt(_authenticationUri(redirectionUri));
+ // Use code to obtain credentials
+ return _obtainAccessCredentialsUsingCode(code, redirectionUri);
+ }
+}
+
+// TODO: Server app flow is missing here.
diff --git a/googleapis_auth/lib/src/oauth2_flows/implicit.dart b/googleapis_auth/lib/src/oauth2_flows/implicit.dart
new file mode 100644
index 0000000..4c1dbfe
--- /dev/null
+++ b/googleapis_auth/lib/src/oauth2_flows/implicit.dart
@@ -0,0 +1,261 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.implicit_gapi_flow;
+
+import 'dart:async';
+import 'dart:html' as html;
+import 'dart:js' as js;
+
+import '../../auth.dart';
+import '../utils.dart';
+
+// This will be overridden by tests.
+String gapiUrl = 'https://apis.google.com/js/client.js';
+
+// According to the CSP3 spec a nonce must be a valid base64 string.
+// https://w3c.github.io/webappsec-csp/#grammardef-base64-value
+final _noncePattern = RegExp('^[\\w+\/_-]+[=]{0,2}\$');
+
+/// This class performs the implicit browser-based oauth2 flow.
+///
+/// It has to be used in two steps:
+///
+/// 1. First call initialize() and wait until the Future completes successfully
+/// - loads the 'gapi' JavaScript library into the current document
+/// - wait until the library signals it is ready
+///
+/// 2. Call login() as often as needed.
+/// - will call the 'gapi' JavaScript lib to trigger an oauth2 browser flow
+/// => This might create a popup which asks the user for consent.
+/// - will wait until the flow is completed (successfully or not)
+/// => Completes with AccessToken or an Exception.
+/// 3. Call loginHybrid() as often as needed.
+/// - will call the 'gapi' JavaScript lib to trigger an oauth2 browser flow
+/// => This might create a popup which asks the user for consent.
+/// - will wait until the flow is completed (successfully or not)
+/// => Completes with a tuple [AccessCredentials cred, String authCode]
+/// or an Exception.
+class ImplicitFlow {
+ static const callbackTimeout = Duration(seconds: 20);
+
+ final String _clientId;
+ final List<String> _scopes;
+
+ /// The pending result of an earlier call to [initialize], if any.
+ ///
+ /// There can be multiple [ImplicitFlow] objects in an application,
+ /// but the gapi JS library should only ever be loaded once. If
+ /// it's called again while a previous initialization is still pending,
+ /// this will be returned.
+ static Future<void>? _pendingInitialization;
+
+ ImplicitFlow(this._clientId, this._scopes);
+
+ /// Readies the flow for calls to [login] by loading the 'gapi'
+ /// JavaScript library, or returning the [Future] of a pending
+ /// initialization if any object has called this method already.
+ Future<void> initialize() {
+ if (_pendingInitialization != null) {
+ return _pendingInitialization!;
+ }
+
+ final completer = Completer();
+
+ final timeout = Timer(callbackTimeout, () {
+ _pendingInitialization = null;
+ completer.completeError(Exception(
+ 'Timed out while waiting for the gapi.auth library to load.'));
+ });
+
+ js.context['dartGapiLoaded'] = () {
+ timeout.cancel();
+ try {
+ final gapi = js.context['gapi']['auth'];
+ try {
+ gapi.callMethod('init', [completer.complete]);
+ // ignore: avoid_catching_errors
+ } on NoSuchMethodError {
+ throw StateError('gapi.auth not loaded.');
+ }
+ } catch (error, stack) {
+ _pendingInitialization = null;
+ if (!completer.isCompleted) {
+ completer.completeError(error, stack);
+ }
+ }
+ };
+
+ final script = _createScript();
+ script.src = '$gapiUrl?onload=dartGapiLoaded';
+ script.onError.first.then((errorEvent) {
+ timeout.cancel();
+ _pendingInitialization = null;
+ if (!completer.isCompleted) {
+ // script loading errors can still happen after timeouts
+ completer.completeError(Exception('Failed to load gapi library.'));
+ }
+ });
+ html.document.body!.append(script);
+
+ _pendingInitialization = completer.future;
+ return completer.future;
+ }
+
+ Future<LoginResult> loginHybrid(
+ {bool force = false, bool immediate = false, String? loginHint}) =>
+ _login(force, immediate, true, loginHint, null);
+
+ Future<AccessCredentials> login(
+ {bool force = false,
+ bool immediate = false,
+ String? loginHint,
+ List<ResponseType>? responseTypes}) async =>
+ (await _login(force, immediate, false, loginHint, responseTypes))
+ .credential;
+
+ // Completes with either credentials or a tuple of credentials and authCode.
+ // hybrid => [AccessCredentials credentials, String authCode]
+ // !hybrid => AccessCredentials
+ //
+ // Alternatively, the response types can be set directly if `hybrid` is not
+ // set to `true`.
+ Future<LoginResult> _login(bool force, bool immediate, bool hybrid,
+ String? loginHint, List<ResponseType>? responseTypes) {
+ assert(hybrid != true || responseTypes?.isNotEmpty != true);
+
+ final completer = Completer<LoginResult>();
+
+ final gapi = js.context['gapi']['auth'];
+
+ final json = {
+ 'client_id': _clientId,
+ 'immediate': immediate,
+ 'approval_prompt': force ? 'force' : 'auto',
+ 'response_type': responseTypes?.isNotEmpty == true
+ ? responseTypes!.map(_responseTypeToString).join(' ')
+ : hybrid
+ ? 'code token'
+ : 'token',
+ 'scope': _scopes.join(' '),
+ 'access_type': hybrid ? 'offline' : 'online',
+ };
+
+ if (loginHint != null) {
+ json['login_hint'] = loginHint;
+ }
+
+ gapi.callMethod('authorize', [
+ js.JsObject.jsify(json),
+ (jsTokenObject) {
+ final tokenType = jsTokenObject['token_type'];
+ final token = jsTokenObject['access_token'];
+ final expiresInRaw = jsTokenObject['expires_in'];
+ final code = jsTokenObject['code'];
+ final error = jsTokenObject['error'];
+ final idToken = jsTokenObject['id_token'];
+
+ int? expiresIn;
+ if (expiresInRaw is String) {
+ expiresIn = int.parse(expiresInRaw);
+ }
+ if (error != null) {
+ completer.completeError(
+ UserConsentException('Failed to get user consent: $error.'),
+ );
+ } else if (token == null ||
+ expiresIn == null ||
+ tokenType != 'Bearer') {
+ completer.completeError(
+ Exception(
+ 'Failed to obtain user consent. Invalid server response.'),
+ );
+ } else if (responseTypes?.contains(ResponseType.idToken) == true &&
+ idToken?.isNotEmpty != true) {
+ completer.completeError(
+ Exception('Expected to get id_token, but did not.'));
+ } else {
+ final accessToken =
+ AccessToken('Bearer', token, expiryDate(expiresIn));
+ final credentials =
+ AccessCredentials(accessToken, null, _scopes, idToken: idToken);
+
+ if (hybrid) {
+ if (code == null) {
+ completer.completeError(
+ Exception('Expected to get auth code '
+ 'from server in hybrid flow, but did not.'),
+ );
+ return;
+ }
+ completer.complete(LoginResult(credentials, code: code));
+ } else {
+ completer.complete(LoginResult(credentials));
+ }
+ }
+ }
+ ]);
+
+ return completer.future;
+ }
+}
+
+class LoginResult {
+ final AccessCredentials credential;
+ final String? code;
+
+ LoginResult(this.credential, {this.code});
+}
+
+/// Convert [responseType] to string value expected by `gapi.auth.authorize`.
+String _responseTypeToString(ResponseType responseType) {
+ String result;
+
+ switch (responseType) {
+ case ResponseType.code:
+ result = 'code';
+ break;
+
+ case ResponseType.idToken:
+ result = 'id_token';
+ break;
+
+ case ResponseType.permission:
+ result = 'permission';
+ break;
+
+ case ResponseType.token:
+ result = 'token';
+ break;
+
+ default:
+ throw ArgumentError('Unknown response type: $responseType');
+ }
+
+ return result;
+}
+
+/// Creates a script that will run properly when strict CSP is enforced.
+///
+/// More specifically, the script has the correct `nonce` value set.
+final html.ScriptElement Function() _createScript = (() {
+ final nonce = _getNonce();
+ if (nonce == null) return () => html.ScriptElement();
+
+ return () => html.ScriptElement()..nonce = nonce;
+})();
+
+/// Returns CSP nonce, if set for any script tag.
+String? _getNonce({html.Window? window}) {
+ final currentWindow = window ?? html.window;
+ final elements = currentWindow.document.querySelectorAll('script');
+ for (final element in elements) {
+ final nonceValue =
+ (element as html.HtmlElement).nonce ?? element.attributes['nonce'];
+ if (nonceValue != null && _noncePattern.hasMatch(nonceValue)) {
+ return nonceValue;
+ }
+ }
+ return null;
+}
diff --git a/googleapis_auth/lib/src/oauth2_flows/jwt.dart b/googleapis_auth/lib/src/oauth2_flows/jwt.dart
new file mode 100644
index 0000000..ecc14a4
--- /dev/null
+++ b/googleapis_auth/lib/src/oauth2_flows/jwt.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library jwt_token_generator;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+
+import '../../auth.dart';
+import '../crypto/rsa.dart';
+import '../crypto/rsa_sign.dart';
+import '../http_client_base.dart';
+import '../utils.dart';
+
+class JwtFlow {
+ // All details are described at:
+ // https://developers.google.com/accounts/docs/OAuth2ServiceAccount
+ // JSON Web Signature (JWS) requires signing a string with a private key.
+
+ static const _googleOauth2TokenUrl =
+ 'https://accounts.google.com/o/oauth2/token';
+
+ final String _clientEmail;
+ final RS256Signer _signer;
+ final List<String> _scopes;
+ final String? _user;
+ final http.Client _client;
+
+ JwtFlow(this._clientEmail, RSAPrivateKey key, this._user, this._scopes,
+ this._client)
+ : _signer = RS256Signer(key);
+
+ Future<AccessCredentials> run() async {
+ final timestamp = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000 -
+ maxExpectedTimeDiffInSeconds;
+
+ Map<String, Object> jwtHeader() => {'alg': 'RS256', 'typ': 'JWT'};
+
+ Map<String, Object> jwtClaimSet() {
+ final claimSet = {
+ 'iss': _clientEmail,
+ 'scope': _scopes.join(' '),
+ 'aud': _googleOauth2TokenUrl,
+ 'exp': timestamp + 3600,
+ 'iat': timestamp,
+ if (_user != null) 'sub': _user!,
+ };
+ return claimSet;
+ }
+
+ final jwtHeaderBase64 = _base64url(ascii.encode(jsonEncode(jwtHeader())));
+ final jwtClaimSetBase64 =
+ _base64url(utf8.encode(jsonEncode(jwtClaimSet())));
+ final jwtSignatureInput = '$jwtHeaderBase64.$jwtClaimSetBase64';
+ final jwtSignatureInputInBytes = ascii.encode(jwtSignatureInput);
+
+ final signature = _signer.sign(jwtSignatureInputInBytes);
+ final jwt = '$jwtSignatureInput.${_base64url(signature)}';
+
+ const uri = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
+ final requestParameters = 'grant_type=${Uri.encodeComponent(uri)}&'
+ 'assertion=${Uri.encodeComponent(jwt)}';
+
+ final body = Stream<List<int>>.fromIterable(
+ <List<int>>[utf8.encode(requestParameters)]);
+ final request = RequestImpl('POST', Uri.parse(_googleOauth2TokenUrl), body);
+ request.headers['content-type'] = contentTypeUrlEncoded;
+
+ final httpResponse = await _client.send(request);
+ final response = await httpResponse.stream
+ .transform(utf8.decoder)
+ .transform(json.decoder)
+ .first as Map;
+ final tokenType = response['token_type'];
+ final token = response['access_token'];
+ final expiresIn = response['expires_in'];
+ final error = response['error'];
+
+ if (httpResponse.statusCode != 200 && error != null) {
+ throw Exception('Unable to obtain credentials. Error: $error.');
+ }
+
+ if (tokenType != 'Bearer' || token == null || expiresIn is! int) {
+ throw Exception(
+ 'Unable to obtain credentials. Invalid response from server.');
+ }
+ final accessToken = AccessToken(tokenType, token, expiryDate(expiresIn));
+ return AccessCredentials(accessToken, null, _scopes);
+ }
+
+ String _base64url(List<int> bytes) =>
+ base64Url.encode(bytes).replaceAll('=', '');
+}
diff --git a/googleapis_auth/lib/src/oauth2_flows/metadata_server.dart b/googleapis_auth/lib/src/oauth2_flows/metadata_server.dart
new file mode 100644
index 0000000..0330efd
--- /dev/null
+++ b/googleapis_auth/lib/src/oauth2_flows/metadata_server.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.metadata_server_flow;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:http/http.dart' as http;
+
+import '../../auth.dart';
+import '../utils.dart';
+
+/// Obtains access credentials form the metadata server.
+///
+/// Using this class assumes that the current program is running a
+/// ComputeEngine VM. It will retrieve the current access token from the
+/// metadata server, looking first for one set in the environment under
+/// `$GCE_METADATA_HOST`.
+class MetadataServerAuthorizationFlow {
+ static const _headers = {'Metadata-Flavor': 'Google'};
+ static const _serviceAccountUrlInfix =
+ 'computeMetadata/v1/instance/service-accounts';
+ static const _defaultMetadataHost = 'metadata';
+ static const _gceMetadataHostEnvVar = 'GCE_METADATA_HOST';
+
+ final String email;
+ final Uri _scopesUrl;
+ final Uri _tokenUrl;
+ final http.Client _client;
+
+ factory MetadataServerAuthorizationFlow(http.Client client,
+ {String email = 'default'}) {
+ final encodedEmail = Uri.encodeComponent(email);
+
+ final metadataHost =
+ Platform.environment[_gceMetadataHostEnvVar] ?? _defaultMetadataHost;
+ final serviceAccountPrefix =
+ 'http://$metadataHost/$_serviceAccountUrlInfix';
+
+ final scopesUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/scopes');
+ final tokenUrl = Uri.parse('$serviceAccountPrefix/$encodedEmail/token');
+ return MetadataServerAuthorizationFlow._(
+ client, email, scopesUrl, tokenUrl);
+ }
+
+ MetadataServerAuthorizationFlow._(
+ this._client, this.email, this._scopesUrl, this._tokenUrl);
+
+ Future<AccessCredentials> run() async {
+ final results = await Future.wait([_getToken(), _getScopes()]);
+ final token = results.first as Map<dynamic, dynamic>;
+ final scopesString = results.last as String;
+
+ final json = token;
+ final scopes = scopesString
+ .replaceAll('\n', ' ')
+ .split(' ')
+ .where((part) => part.isNotEmpty)
+ .toList();
+
+ final type = json['token_type'];
+ final accessToken = json['access_token'];
+ final expiresIn = json['expires_in'];
+ final error = json['error'];
+
+ if (error != null) {
+ throw Exception('Error while obtaining credentials from metadata '
+ 'server. Error message: $error.');
+ }
+
+ if (type != 'Bearer' || accessToken == null || expiresIn is! int) {
+ throw Exception('Invalid response from metadata server.');
+ }
+
+ return AccessCredentials(
+ AccessToken(type, accessToken, expiryDate(expiresIn)), null, scopes);
+ }
+
+ Future<Map> _getToken() async {
+ final response = await _client.get(_tokenUrl, headers: _headers);
+ return jsonDecode(response.body);
+ }
+
+ Future<String> _getScopes() async {
+ final response = await _client.get(_scopesUrl, headers: _headers);
+ return response.body;
+ }
+}
diff --git a/googleapis_auth/lib/src/typedefs.dart b/googleapis_auth/lib/src/typedefs.dart
new file mode 100644
index 0000000..78b337e
--- /dev/null
+++ b/googleapis_auth/lib/src/typedefs.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.typedefs;
+
+/// Function for directing the user or it's user-agent to [uri].
+///
+/// The user is required to go to [uri] and either approve or decline the
+/// application's request for access resources on his behalf.
+typedef PromptUserForConsent = void Function(String uri);
+
+/// Function for directing the user or it's user-agent to [uri].
+///
+/// The user is required to go to [uri] and either approve or decline the
+/// application's request for access resources on his behalf.
+///
+/// The user will be given an authorization code. This function should complete
+/// with this authorization code. If the user declined to give access this
+/// function should complete with an error.
+typedef PromptUserForConsentManual = Future<String> Function(String uri);
diff --git a/googleapis_auth/lib/src/utils.dart b/googleapis_auth/lib/src/utils.dart
new file mode 100644
index 0000000..84ded61
--- /dev/null
+++ b/googleapis_auth/lib/src/utils.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.utils;
+
+/// Due to differences of clock speed, network latency, etc. we
+/// will shorten expiry dates by 20 seconds.
+const maxExpectedTimeDiffInSeconds = 20;
+
+/// Constructs a [DateTime] which is [seconds] seconds from now with
+/// an offset of [maxExpectedTimeDiffInSeconds]. Result is UTC time.
+DateTime expiryDate(int seconds) => DateTime.now()
+ .toUtc()
+ .add(Duration(seconds: seconds - maxExpectedTimeDiffInSeconds));
+
+/// Constant for the 'application/x-www-form-urlencoded' content type
+const contentTypeUrlEncoded =
+ 'application/x-www-form-urlencoded; charset=utf-8';
diff --git a/googleapis_auth/mono_pkg.yaml b/googleapis_auth/mono_pkg.yaml
new file mode 100644
index 0000000..528c298
--- /dev/null
+++ b/googleapis_auth/mono_pkg.yaml
@@ -0,0 +1,11 @@
+dart:
+- dev
+
+stages:
+- analyze_and_format:
+ - group:
+ - dartfmt
+ - dartanalyzer: --fatal-infos .
+- unittest:
+ - test: -p vm
+ - test: -p chrome
diff --git a/googleapis_auth/pubspec.yaml b/googleapis_auth/pubspec.yaml
new file mode 100644
index 0000000..6a2fb8f
--- /dev/null
+++ b/googleapis_auth/pubspec.yaml
@@ -0,0 +1,22 @@
+name: googleapis_auth
+version: 0.2.12+2-dev
+description: Obtain Access credentials for Google services using OAuth 2.0
+repository: https://github.com/dart-lang/googleapis
+environment:
+ sdk: '>=2.12.0-0 <3.0.0'
+
+dependencies:
+ crypto: ^3.0.0-nullsafety.0
+ # Not published yet.
+ http: any
+
+dev_dependencies:
+ test: ^1.16.0-nullsafety.9
+
+dependency_overrides:
+ http:
+ git:
+ url: git://github.com/dart-lang/http.git
+ ref: 3845753a54624b070828cb3eff7a6c2a4e046cfb
+ # Needs bump in `packge:shelf` - blocked by pkg:http
+ http_parser: ^4.0.0-nullsafety
diff --git a/googleapis_auth/test/adc_test.dart b/googleapis_auth/test/adc_test.dart
new file mode 100644
index 0000000..efd7ea5
--- /dev/null
+++ b/googleapis_auth/test/adc_test.dart
@@ -0,0 +1,123 @@
+@TestOn('vm')
+library googleapis_auth.adc_test;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:googleapis_auth/src/adc_utils.dart'
+ show fromApplicationsCredentialsFile;
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ test('fromApplicationsCredentialsFile', () async {
+ final tmp = await Directory.systemTemp.createTemp('googleapis_auth-test');
+ try {
+ final credsFile = File.fromUri(tmp.uri.resolve('creds.json'));
+ await credsFile.writeAsString(json.encode({
+ 'client_id': 'id',
+ 'client_secret': 'secret',
+ 'refresh_token': 'refresh',
+ 'type': 'authorized_user'
+ }));
+ final c = await fromApplicationsCredentialsFile(
+ credsFile,
+ 'test-credentials-file',
+ [],
+ mockClient((Request request) async {
+ final url = request.url.toString();
+ if (url == 'https://accounts.google.com/o/oauth2/token') {
+ expect(request.method, equals('POST'));
+ expect(
+ request.body,
+ equals('client_id=id&'
+ 'client_secret=secret&'
+ 'refresh_token=refresh&'
+ 'grant_type=refresh_token'));
+ final body = jsonEncode({
+ 'token_type': 'Bearer',
+ 'access_token': 'atoken',
+ 'expires_in': 3600,
+ });
+ return Response(body, 200, headers: _jsonContentType);
+ }
+ if (url == 'https://storage.googleapis.com/b/bucket/o/obj') {
+ expect(request.method, equals('GET'));
+ expect(request.headers['Authorization'], equals('Bearer atoken'));
+ expect(request.headers['X-Goog-User-Project'], isNull);
+ return Response('hello world', 200);
+ }
+ return Response('bad', 404);
+ }, expectClose: false),
+ );
+ expect(c.credentials.accessToken.data, equals('atoken'));
+
+ final r =
+ await c.get(Uri.https('storage.googleapis.com', '/b/bucket/o/obj'));
+ expect(r.statusCode, equals(200));
+ expect(r.body, equals('hello world'));
+
+ c.close();
+ } finally {
+ await tmp.delete(recursive: true);
+ }
+ });
+
+ test('fromApplicationsCredentialsFile w. quota_project_id', () async {
+ final tmp = await Directory.systemTemp.createTemp('googleapis_auth-test');
+ try {
+ final credsFile = File.fromUri(tmp.uri.resolve('creds.json'));
+ await credsFile.writeAsString(json.encode({
+ 'client_id': 'id',
+ 'client_secret': 'secret',
+ 'refresh_token': 'refresh',
+ 'type': 'authorized_user',
+ 'quota_project_id': 'project'
+ }));
+ final c = await fromApplicationsCredentialsFile(
+ credsFile,
+ 'test-credentials-file',
+ [],
+ mockClient((Request request) async {
+ final url = request.url.toString();
+ if (url == 'https://accounts.google.com/o/oauth2/token') {
+ expect(request.method, equals('POST'));
+ expect(
+ request.body,
+ equals('client_id=id&'
+ 'client_secret=secret&'
+ 'refresh_token=refresh&'
+ 'grant_type=refresh_token'));
+ final body = jsonEncode({
+ 'token_type': 'Bearer',
+ 'access_token': 'atoken',
+ 'expires_in': 3600,
+ });
+ return Response(body, 200, headers: _jsonContentType);
+ }
+ if (url == 'https://storage.googleapis.com/b/bucket/o/obj') {
+ expect(request.method, equals('GET'));
+ expect(request.headers['Authorization'], equals('Bearer atoken'));
+ expect(request.headers['X-Goog-User-Project'], equals('project'));
+ return Response('hello world', 200);
+ }
+ return Response('bad', 404);
+ }, expectClose: false),
+ );
+ expect(c.credentials.accessToken.data, equals('atoken'));
+
+ final r =
+ await c.get(Uri.https('storage.googleapis.com', '/b/bucket/o/obj'));
+ expect(r.statusCode, equals(200));
+ expect(r.body, equals('hello world'));
+
+ c.close();
+ } finally {
+ await tmp.delete(recursive: true);
+ }
+ });
+}
+
+const _jsonContentType = {'content-type': 'application/json'};
diff --git a/googleapis_auth/test/crypto/asn1_test.dart b/googleapis_auth/test/crypto/asn1_test.dart
new file mode 100644
index 0000000..cdf7f41
--- /dev/null
+++ b/googleapis_auth/test/crypto/asn1_test.dart
@@ -0,0 +1,150 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.test.asn1_test;
+
+import 'dart:typed_data';
+
+import 'package:googleapis_auth/src/crypto/asn1.dart';
+import 'package:test/test.dart';
+
+void main() {
+ void expectArgumentError(List<int> bytes) {
+ expect(() => ASN1Parser.parse(Uint8List.fromList(bytes)),
+ throwsA(isArgumentError));
+ }
+
+ void invalidLenTest(int tagBytes) {
+ test('invalid-len', () {
+ expectArgumentError([tagBytes]);
+ expectArgumentError([tagBytes, 0x07]);
+ expectArgumentError([tagBytes, 0x82]);
+ expectArgumentError([tagBytes, 0x82, 1]);
+ expectArgumentError([tagBytes, 0x01, 1, 2, 3, 4]);
+ });
+ }
+
+ group('asn1-parser', () {
+ group('sequence', () {
+ test('empty', () {
+ final sequenceBytes = [ASN1Parser.sequenceTag, 0];
+ final sequence = ASN1Parser.parse(Uint8List.fromList(sequenceBytes));
+ expect(sequence is ASN1Sequence, isTrue);
+ expect((sequence as ASN1Sequence).objects, isEmpty);
+ });
+
+ test('one-element', () {
+ final sequenceBytes = [
+ ASN1Parser.sequenceTag,
+ 1,
+ ASN1Parser.nullTag,
+ 0
+ ];
+ final sequence = ASN1Parser.parse(Uint8List.fromList(sequenceBytes));
+ expect(sequence is ASN1Sequence, isTrue);
+ expect((sequence as ASN1Sequence).objects, hasLength(1));
+ expect(sequence.objects[0] is ASN1Null, isTrue);
+ });
+
+ test('many-elements', () {
+ final sequenceBytes = [ASN1Parser.sequenceTag, 0x82, 0x01, 0x00];
+ for (var i = 0; i < 128; i++) {
+ sequenceBytes.addAll([ASN1Parser.nullTag, 0]);
+ }
+
+ final sequence = ASN1Parser.parse(Uint8List.fromList(sequenceBytes));
+ expect(sequence is ASN1Sequence, isTrue);
+ expect((sequence as ASN1Sequence).objects.length, equals(128));
+ for (var i = 0; i < 128; i++) {
+ expect(sequence.objects[i] is ASN1Null, isTrue);
+ }
+ });
+
+ invalidLenTest(ASN1Parser.sequenceTag);
+ });
+
+ group('integer', () {
+ test('small', () {
+ for (var i = 0; i < 256; i++) {
+ final integerBytes = [ASN1Parser.integerTag, 1, i];
+ final integer =
+ ASN1Parser.parse(Uint8List.fromList(integerBytes)) as ASN1Integer;
+ expect(integer.integer, BigInt.from(i));
+ }
+ });
+
+ test('multi-byte', () {
+ final integerBytes = [ASN1Parser.integerTag, 3, 1, 2, 3];
+ final integer = ASN1Parser.parse(Uint8List.fromList(integerBytes));
+ expect(integer is ASN1Integer, isTrue);
+ expect((integer as ASN1Integer).integer, BigInt.from(0x010203));
+ });
+
+ invalidLenTest(ASN1Parser.integerTag);
+ });
+
+ group('octet-string', () {
+ test('small', () {
+ final octetStringBytes = [ASN1Parser.octetStringTag, 3, 1, 2, 3];
+ final octetString =
+ ASN1Parser.parse(Uint8List.fromList(octetStringBytes));
+ expect(octetString is ASN1OctetString, isTrue);
+ expect((octetString as ASN1OctetString).bytes, equals([1, 2, 3]));
+ });
+
+ test('large', () {
+ final octetStringBytes = [ASN1Parser.octetStringTag, 0x82, 0x01, 0x00];
+ for (var i = 0; i < 256; i++) {
+ octetStringBytes.add(i % 256);
+ }
+
+ final octetString =
+ ASN1Parser.parse(Uint8List.fromList(octetStringBytes));
+ expect(octetString is ASN1OctetString, isTrue);
+ final castedOctetString = octetString as ASN1OctetString;
+ for (var i = 0; i < 256; i++) {
+ expect(castedOctetString.bytes[i], equals(i % 256));
+ }
+ });
+
+ invalidLenTest(ASN1Parser.octetStringTag);
+ });
+
+ group('oid', () {
+ // NOTE: Currently the oid is parsed as normal bytes, so we don't validate
+ // the oid structure.
+ test('small', () {
+ final objIdBytes = [ASN1Parser.objectIdTag, 3, 1, 2, 3];
+ final objId = ASN1Parser.parse(Uint8List.fromList(objIdBytes));
+ expect(objId is ASN1ObjectIdentifier, isTrue);
+ expect((objId as ASN1ObjectIdentifier).bytes, equals([1, 2, 3]));
+ });
+
+ test('large', () {
+ final objIdBytes = [ASN1Parser.objectIdTag, 0x82, 0x01, 0x00];
+ for (var i = 0; i < 256; i++) {
+ objIdBytes.add(i % 256);
+ }
+
+ final objId = ASN1Parser.parse(Uint8List.fromList(objIdBytes));
+ expect(objId is ASN1ObjectIdentifier, isTrue);
+ final castedObjId = objId as ASN1ObjectIdentifier;
+ for (var i = 0; i < 256; i++) {
+ expect(castedObjId.bytes[i], equals(i % 256));
+ }
+ });
+
+ invalidLenTest(ASN1Parser.objectIdTag);
+ });
+ });
+
+ test('null', () {
+ final objId =
+ ASN1Parser.parse(Uint8List.fromList([ASN1Parser.nullTag, 0x00]));
+ expect(objId is ASN1Null, isTrue);
+
+ expectArgumentError([ASN1Parser.nullTag]);
+ expectArgumentError([ASN1Parser.nullTag, 0x01]);
+ });
+}
diff --git a/googleapis_auth/test/crypto/pem_test.dart b/googleapis_auth/test/crypto/pem_test.dart
new file mode 100644
index 0000000..59ddc17
--- /dev/null
+++ b/googleapis_auth/test/crypto/pem_test.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:googleapis_auth/src/crypto/pem.dart';
+import 'package:test/test.dart';
+
+import '../test_utils.dart';
+
+void main() {
+ group('pem', () {
+ test('null', () {
+ expect(() => keyFromString(''), throwsA(isArgumentError));
+ });
+
+ test('pem--key-from-string', () {
+ final key = keyFromString(testPrivateKeyString);
+ expect(
+ key.p,
+ equals(BigInt.parse(
+ '170185878019789847607218833833962851295383479739128068911675681859184825725303329240997154492057125840628991571181411414164882361723231273391547091096391845233984484218520948165420605211532206383859989286454330226302062891556391372178426684136261758077913279309249468965000813860343415338472623037185763380093')));
+ expect(
+ key.q,
+ equals(BigInt.parse(
+ '136634937867625346722869734066327766542560453705266659651284573193680854438532412351608161985232086174999341126075829838477923122149398705411098928405144549034231120055200290950893136823181693383585861140730929930114638604738429489364496581584222788741142343940831827356789459450282075298628271623617861448279')));
+ expect(
+ key.n,
+ equals(BigInt.parse(
+ '23253336869181252005308127869627478511861722018560725538542603352356752658510633204810959681459083455055115233727694253121121138828979138624495569601457246561359553177524606534054439784597124760679930421448728375265700767584567585959695707287695356045087640902894887625471020788794811661755081070077086649519865067918501869783817745592796089436450623267438942174934673417424553992577792939276705103879103955476795626469391055763713456179432199172562526422301070938382514265029982800538033050279129668807032677927531973249309321914500317007151921938466293582589451642241740444272968677617027011566610435323463337709947')));
+ expect(
+ key.d,
+ equals(BigInt.parse(
+ '21186554940454261253047269959735660724480631477978821785517431853394668885438560354085051566279884512080977781045029208574826211785037495240030508751426142586201712610225510861978099522679761260199887167944008250970681053969661407950094604171122649803382413195502685962008111346880629170494825836648656453852203519401121722270587408277317819537925146228717860401265662699719826243356610955461998054615517371279631680512102389979478015385709644867750888484550190071229275090881149432467365050794063725847869274512118390103343213000471284707060203072264487986083004823016463235156640750689592865369834958756866148520449')));
+ expect(key.e, equals(BigInt.from(65537)));
+ expect(
+ key.dmp1,
+ equals(BigInt.parse(
+ '8112374428701702609593842209702915108210293280208677346843383586799722226617751812699316578927727255231777006398991855865405686833748485558923861522271817820635175987589597358267451526325993144989526626651865780047418167954318425419006133348210655541684866328365584952723843668457708310075048817739114161457')));
+ expect(
+ key.dmq1,
+ equals(BigInt.parse(
+ '69064888333930830841944331910451194321610695483381427808232052980561601308959263072336597373770287299070802348040252301131546443496698520136006747353055884093824470361301555431744464097251017848208627523520965497274938325818544542688522182250340240209771921627903870254182590341478772425006618460954711021211')));
+ expect(
+ key.coeff,
+ equals(BigInt.parse(
+ '16726959063327324857338379758571748557044292252371297447561270320393087678399207080059961434627453370656491757664831584315003981946034135341817305303511530360890203726058358401094205679273808207987503167082629712433452873772120961093571912870024590300080209978748890272607981079166485164486378666155431958545')));
+ });
+ });
+}
diff --git a/googleapis_auth/test/crypto/rsa_sign_test.dart b/googleapis_auth/test/crypto/rsa_sign_test.dart
new file mode 100644
index 0000000..3ee884d
--- /dev/null
+++ b/googleapis_auth/test/crypto/rsa_sign_test.dart
@@ -0,0 +1,90 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.test.crypto_rsa_sign_test;
+
+import 'dart:convert';
+
+import 'package:googleapis_auth/src/crypto/rsa_sign.dart';
+import 'package:test/test.dart';
+
+import '../test_utils.dart';
+
+void main() {
+ group('rsa-sha256-signer', () {
+ final signer = RS256Signer(testPrivateKey);
+
+ // NOTE:
+ // The signatures can be regenerated via the openssl commandline utility:
+ // $ cat plaintext | openssl dgst -sha256 -sign key.pem > ciphertext
+ // e.g.
+ // $ echo -n "hello world"|openssl dgst -sha256 -sign key.pem|hexdump -v -C
+ // 00000000 59 9d 6f 81 c1 0f d6 f1 58 46 2d 4d c9 b8 69 1d
+ // 00000010 b1 e0 e0 26 a4 de 49 d8 4f 5a ac db 81 ab 10 27
+ // 00000020 3a f4 5a f8 bb da a9 84 be c7 5a fb b9 2e 0a 66
+ // 00000030 8f 78 d5 cb c9 82 0b 57 36 fc bc 42 1b f5 fa 76
+ // 00000040 b7 01 4c bc 2d b9 fe 20 55 62 f5 87 8c bc e3 58
+ // 00000050 a6 c6 8a ef 16 c8 4a 85 01 6e df 05 43 c8 ef 35
+ // 00000060 37 9f 1b 29 57 eb c7 93 89 75 f5 65 81 0a 6c 8c
+ // 00000070 44 35 ad 73 89 90 53 42 26 f3 31 a9 06 f1 32 20
+ // 00000080 48 a3 e1 68 3d 86 67 45 74 19 91 75 c9 28 ca 8b
+ // 00000090 33 63 ed a2 b1 90 e6 e1 0a 1f 87 ec 02 f8 92 03
+ // 000000a0 cf 0e 30 49 b0 f1 72 29 a3 9c 2e cc 7c 87 65 11
+ // 000000b0 1f 38 34 d3 3e fe af 8e 31 f0 10 1f f5 71 dd 90
+ // 000000c0 f6 c7 ba 5d 10 0c 63 eb a4 3c a5 17 9a 99 52 2d
+ // 000000d0 b6 27 96 8c e2 44 63 35 1f 04 6f b8 31 e6 d4 47
+ // 000000e0 31 0d 3c 36 6c bf 14 df dc 2d 53 c7 ca d1 ec 6d
+ // 000000f0 95 37 2f 86 14 da 6c 04 a1 fd 45 fa 95 e0 04 bf
+ test('encrypt-hello-world', () {
+ expect(
+ signer.sign(ascii.encode('hello world')),
+ equals([
+ 89, 157, 111, 129, 193, 15, 214, 241, 88, 70, 45, 77, 201, 184, //!!
+ 105, 29, 177, 224, 224, 38, 164, 222, 73, 216, 79, 90, 172, 219,
+ 129, 171, 16, 39, 58, 244, 90, 248, 187, 218, 169, 132, 190, 199,
+ 90, 251, 185, 46, 10, 102, 143, 120, 213, 203, 201, 130, 11, 87, 54,
+ 252, 188, 66, 27, 245, 250, 118, 183, 1, 76, 188, 45, 185, 254, 32,
+ 85, 98, 245, 135, 140, 188, 227, 88, 166, 198, 138, 239, 22,
+ 200, 74, 133, 1, 110, 223, 5, 67, 200, 239, 53, 55, 159, 27, 41, 87,
+ 235, 199, 147, 137, 117, 245, 101, 129, 10, 108, 140, 68, 53, 173,
+ 115, 137, 144, 83, 66, 38, 243, 49, 169, 6, 241, 50, 32, 72, 163,
+ 225, 104, 61, 134, 103, 69, 116, 25, 145, 117, 201, 40, 202, 139,
+ 51, 99, 237, 162, 177, 144, 230, 225, 10, 31, 135, 236, 2, 248, 146,
+ 3, 207, 14, 48, 73, 176, 241, 114, 41, 163, 156, 46, 204, 124, 135,
+ 101, 17, 31, 56, 52, 211, 62, 254, 175, 142, 49, 240, 16, 31, 245,
+ 113, 221, 144, 246, 199, 186, 93, 16, 12, 99, 235, 164, 60, 165, 23,
+ 154, 153, 82, 45, 182, 39, 150, 140, 226, 68, 99, 53, 31, 4, 111,
+ 184, 49, 230, 212, 71, 49, 13, 60, 54, 108, 191, 20, 223, 220, 45,
+ 83, 199, 202, 209, 236, 109, 149, 55, 47, 134, 20, 218, 108, 4, 161,
+ 253, 69, 250, 149, 224, 4, 191
+ ]));
+ });
+
+ // $ echo -n ""|openssl dgst -sha256 -sign key.pem|hexdump -v -C
+ test('null-bytes', () {
+ expect(
+ signer.sign([]),
+ equals([
+ 113, 99, 2, 245, 156, 215, 253, 172, 157, 46, 126, 165, 174, //!!
+ 158, 186, 213, 211, 85, 118, 63, 208, 122, 196, 214, 154, 221, 92,
+ 105, 27, 29, 153, 35, 91, 111, 5, 10, 82, 213, 179, 41, 165, 122,
+ 227, 145, 217, 108, 249, 153, 116, 80, 140, 238, 158, 140, 142, 118,
+ 224, 10, 225, 58, 77, 210, 27, 66, 177, 165, 228, 40, 225, 211, 140,
+ 254, 31, 242, 230, 223, 21, 199, 221, 113, 146, 46, 213, 20, 63,
+ 148, 140, 144, 245, 105, 193, 124, 206, 235, 191, 252, 138, 155,
+ 148, 175, 185, 160, 98, 102, 156, 197, 29, 80, 202, 49, 26, 173,
+ 176, 53, 202, 13, 204, 180, 180, 190, 152, 223, 199, 65, 9, 173, 82,
+ 167, 12, 244, 127, 141, 8, 103, 155, 213, 2, 53, 83, 179, 157, 101,
+ 190, 205, 85, 58, 50, 89, 255, 11, 67, 18, 232, 252, 229, 197, 200,
+ 228, 130, 104, 250, 228, 19, 178, 183, 45, 156, 22, 73, 229, 170,
+ 163, 179, 116, 21, 149, 31, 81, 253, 100, 132, 46, 216, 143, 134,
+ 185, 96, 75, 57, 139, 21, 131, 114, 221, 124, 47, 104, 92, 235, 254,
+ 62, 69, 126, 117, 170, 141, 64, 121, 181, 101, 69, 135, 115, 102,
+ 74, 157, 233, 127, 139, 14, 79, 137, 156, 248, 117, 114, 205, 142,
+ 60, 8, 116, 77, 182, 28, 119, 149, 143, 252, 141, 46, 111, 100, 242,
+ 184, 21, 130, 61, 138, 27, 226, 70, 119, 195, 223, 180, 121
+ ]));
+ });
+ });
+}
diff --git a/googleapis_auth/test/crypto/rsa_test.dart b/googleapis_auth/test/crypto/rsa_test.dart
new file mode 100644
index 0000000..441fa73
--- /dev/null
+++ b/googleapis_auth/test/crypto/rsa_test.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.test.rsa_test;
+
+import 'package:googleapis_auth/src/crypto/rsa.dart';
+import 'package:test/test.dart';
+
+import '../test_utils.dart';
+
+/// 2 << 64
+final _bigNumber = BigInt.parse('20000000000000000', radix: 16);
+
+void main() {
+ group('rsa-algorithm', () {
+ test('integer-to-bytes', () {
+ expect(RSAAlgorithm.integer2Bytes(BigInt.one, 1), equals([1]));
+ expect(RSAAlgorithm.integer2Bytes(_bigNumber, 9),
+ equals([2, 0, 0, 0, 0, 0, 0, 0, 0]));
+ expect(RSAAlgorithm.integer2Bytes(_bigNumber, 12),
+ equals([0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0]));
+ expect(() => RSAAlgorithm.integer2Bytes(BigInt.zero, 1),
+ throwsA(isArgumentError));
+ });
+
+ test('bytes-to-integer', () {
+ expect(RSAAlgorithm.bytes2BigInt([1]), equals(BigInt.one));
+ expect(
+ RSAAlgorithm.bytes2BigInt([2, 0, 0, 0, 0, 0, 0, 0, 0]), _bigNumber);
+ });
+
+ test('encrypt', () {
+ final encryptedData = [
+ 155, 24, 116, 247, 12, 118, 240, 206, 240, 138, 136, 193, 3, 73, //!!
+ 241, 63, 212, 100, 97, 46, 55, 113, 119, 95, 240, 219, 136, 211, 3, 4,
+ 43, 137, 213, 92, 233, 57, 172, 80, 179, 117, 83, 88, 249, 75, 17, 20,
+ 195, 51, 25, 97, 248, 217, 41, 117, 55, 63, 5, 252, 42, 133, 82, 73, 52,
+ 219, 255, 38, 137, 209, 83, 57, 245, 188, 180, 233, 249, 144, 100, 153,
+ 145, 14, 94, 2, 229, 165, 131, 178, 195, 178, 95, 244, 153, 196, 130,
+ 39, 158, 143, 98, 181, 223, 184, 68, 198, 201, 203, 89, 15, 41, 185,
+ 226, 64, 226, 161, 43, 228, 90, 58, 152, 203, 142, 133, 113, 120, 97,
+ 78, 149, 86, 214, 135, 29, 29, 190, 16, 47, 210, 1, 213, 86, 100, 116,
+ 187, 11, 255, 224, 6, 6, 206, 60, 138, 24, 179, 245, 248, 200, 45, 167,
+ 100, 78, 131, 204, 120, 22, 73, 116, 127, 65, 201, 15, 177, 250, 4, 73,
+ 245, 67, 119, 21, 54, 255, 227, 206, 37, 216, 13, 8, 109, 238, 215, 22,
+ 63, 163, 155, 33, 148, 254, 113, 17, 68, 65, 48, 82, 43, 240, 249, 87,
+ 19, 87, 162, 148, 169, 93, 22, 135, 125, 134, 187, 48, 93, 52, 20, 182,
+ 56, 93, 0, 175, 193, 213, 144, 29, 44, 240, 226, 91, 54, 178, 241, 240,
+ 85, 53, 148, 172, 138, 107, 131, 14, 157, 183, 137, 46, 130, 51, 233,
+ 26, 217, 230, 133, 217, 76
+ ];
+ expect(
+ RSAAlgorithm.encrypt(
+ testPrivateKey, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 256),
+ equals(encryptedData));
+ });
+ });
+}
diff --git a/googleapis_auth/test/http_client_base_test.dart b/googleapis_auth/test/http_client_base_test.dart
new file mode 100644
index 0000000..7b0933b
--- /dev/null
+++ b/googleapis_auth/test/http_client_base_test.dart
@@ -0,0 +1,132 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.http_client_base_test;
+
+import 'dart:async';
+
+import 'package:googleapis_auth/src/auth_http_utils.dart';
+import 'package:googleapis_auth/src/http_client_base.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+class DelegatingClientImpl extends DelegatingClient {
+ DelegatingClientImpl(Client base, {required bool closeUnderlyingClient})
+ : super(base, closeUnderlyingClient: closeUnderlyingClient);
+
+ @override
+ Future<StreamedResponse> send(BaseRequest request) =>
+ throw UnsupportedError('Not supported');
+}
+
+final _defaultResponse = Response('', 500);
+
+Future<Response> _defaultResponseHandler(Request _) async => _defaultResponse;
+
+void main() {
+ group('http-utils', () {
+ group('delegating-client', () {
+ test('not-close-underlying-client', () {
+ final mock = mockClient(_defaultResponseHandler, expectClose: false);
+ DelegatingClientImpl(mock, closeUnderlyingClient: false).close();
+ });
+
+ test('close-underlying-client', () {
+ final mock = mockClient(_defaultResponseHandler);
+ DelegatingClientImpl(mock, closeUnderlyingClient: true).close();
+ });
+
+ test('close-several-times', () {
+ final mock = mockClient(_defaultResponseHandler);
+ final delegate =
+ DelegatingClientImpl(mock, closeUnderlyingClient: true);
+ delegate.close();
+ expect(delegate.close, throwsA(isStateError));
+ });
+ });
+
+ group('refcounted-client', () {
+ test('not-close-underlying-client', () {
+ final mock = mockClient(_defaultResponseHandler, expectClose: false);
+ final client = RefCountedClient(mock, initialRefCount: 3);
+ client.close();
+ client.close();
+ });
+
+ test('close-underlying-client', () {
+ final mock = mockClient(_defaultResponseHandler);
+ final client = RefCountedClient(mock, initialRefCount: 3);
+ client.close();
+ client.close();
+ client.close();
+ });
+
+ test('acquire-release', () {
+ final mock = mockClient(_defaultResponseHandler);
+ final client = RefCountedClient(mock);
+ client.acquire();
+ client.release();
+ client.acquire();
+ client.release();
+ client.release();
+ });
+
+ test('close-several-times', () {
+ final mock = mockClient(_defaultResponseHandler);
+ final client = RefCountedClient(mock);
+ client.close();
+ expect(client.close, throwsA(isStateError));
+ });
+ });
+
+ group('api-client', () {
+ const key = 'foo%?bar';
+ final keyEncoded = 'key=${Uri.encodeQueryComponent(key)}';
+
+ RequestImpl request(String url) => RequestImpl('GET', Uri.parse(url));
+ Future<Response> responseF() =>
+ Future<Response>.value(Response.bytes([], 200));
+
+ test('no-query-string', () {
+ final mock = mockClient((Request request) {
+ expect('${request.url}', equals('http://localhost/abc?$keyEncoded'));
+ return responseF();
+ });
+
+ final client = ApiKeyClient(mock, key);
+ expect(client.send(request('http://localhost/abc')), completes);
+ client.close();
+ });
+
+ test('with-query-string', () {
+ final mock = mockClient((Request request) {
+ expect(
+ '${request.url}', equals('http://localhost/abc?x&$keyEncoded'));
+ return responseF();
+ });
+
+ final client = ApiKeyClient(mock, key);
+ expect(client.send(request('http://localhost/abc?x')), completes);
+ client.close();
+ });
+
+ test('with-existing-key', () {
+ final mock =
+ mockClient(expectAsync1(_defaultResponseHandler, count: 0));
+
+ final client = ApiKeyClient(mock, key);
+ expect(client.send(request('http://localhost/abc?key=a')),
+ throwsException);
+ client.close();
+ });
+ });
+
+ test('non-closing-client', () {
+ final mock = mockClient(_defaultResponseHandler, expectClose: false);
+ nonClosingClient(mock).close();
+ });
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/auth_code_test.dart b/googleapis_auth/test/oauth2_flows/auth_code_test.dart
new file mode 100644
index 0000000..f223dd0
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/auth_code_test.dart
@@ -0,0 +1,260 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.auth_code_test;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:googleapis_auth/auth.dart';
+import 'package:googleapis_auth/src/oauth2_flows/auth_code.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+import '../test_utils.dart';
+
+typedef RequestHandler = Future<Response> Function(Request _);
+
+void main() {
+ final clientId = ClientId('id', 'secret');
+ final scopes = ['s1', 's2'];
+
+ // Validation + Responses from the authorization server.
+
+ RequestHandler successFullResponse({bool? manual}) =>
+ (Request request) async {
+ expect(request.method, equals('POST'));
+ expect('${request.url}',
+ equals('https://accounts.google.com/o/oauth2/token'));
+ expect(
+ request.headers['content-type']!
+ .startsWith('application/x-www-form-urlencoded'),
+ isTrue);
+
+ final pairs = request.body.split('&');
+ expect(pairs, hasLength(5));
+ expect(pairs[0], equals('grant_type=authorization_code'));
+ expect(pairs[1], equals('code=mycode'));
+ expect(pairs[3], equals('client_id=id'));
+ expect(pairs[4], equals('client_secret=secret'));
+ if (manual!) {
+ expect(pairs[2],
+ equals('redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob'));
+ } else {
+ expect(pairs[2], startsWith('redirect_uri='));
+
+ final url = Uri.parse(
+ Uri.decodeComponent(pairs[2].substring('redirect_uri='.length)));
+ expect(url.scheme, equals('http'));
+ expect(url.host, equals('localhost'));
+ }
+
+ final result = {
+ 'token_type': 'Bearer',
+ 'access_token': 'tokendata',
+ 'expires_in': 3600,
+ 'refresh_token': 'my-refresh-token',
+ 'id_token': 'my-id-token',
+ };
+ return Response(jsonEncode(result), 200);
+ };
+
+ Future<Response> invalidResponse(Request request) {
+ // Missing expires_in field!
+ final result = {
+ 'token_type': 'Bearer',
+ 'access_token': 'tokendata',
+ 'refresh_token': 'my-refresh-token',
+ 'id_token': 'my-id-token',
+ };
+ return Future.value(Response(jsonEncode(result), 200));
+ }
+
+ // Validation functions for user prompt and access credentials.
+
+ void validateAccessCredentials(AccessCredentials credentials) {
+ expect(credentials.accessToken.data, equals('tokendata'));
+ expect(credentials.accessToken.type, equals('Bearer'));
+ expect(credentials.scopes, equals(['s1', 's2']));
+ expect(credentials.refreshToken, equals('my-refresh-token'));
+ expect(credentials.idToken, equals('my-id-token'));
+ expectExpiryOneHourFromNow(credentials.accessToken);
+ }
+
+ Uri validateUserPromptUri(String url, {bool manual = false}) {
+ final uri = Uri.parse(url);
+ expect(uri.scheme, equals('https'));
+ expect(uri.host, equals('accounts.google.com'));
+ expect(uri.path, equals('/o/oauth2/auth'));
+ expect(uri.queryParameters['client_id'], equals(clientId.identifier));
+ expect(uri.queryParameters['response_type'], equals('code'));
+ expect(uri.queryParameters['scope'], equals('s1 s2'));
+ expect(uri.queryParameters['redirect_uri'], isNotNull);
+
+ final redirectUri = Uri.parse(uri.queryParameters['redirect_uri']!);
+
+ if (manual) {
+ expect('$redirectUri', equals('urn:ietf:wg:oauth:2.0:oob'));
+ } else {
+ expect(uri.queryParameters['state'], isNotNull);
+ expect(redirectUri.scheme, equals('http'));
+ expect(redirectUri.host, equals('localhost'));
+ }
+
+ return redirectUri;
+ }
+
+ group('authorization-code-flow', () {
+ group('manual-copy-paste', () {
+ Future<String> manualUserPrompt(String url) {
+ validateUserPromptUri(url, manual: true);
+ return Future.value('mycode');
+ }
+
+ test('successfull', () async {
+ final flow = AuthorizationCodeGrantManualFlow(
+ clientId,
+ scopes,
+ mockClient(successFullResponse(manual: true), expectClose: false),
+ manualUserPrompt);
+ validateAccessCredentials(await flow.run());
+ });
+
+ test('user-exception', () {
+ // We use a TransportException here for convenience.
+ Future<String> manualUserPromptError(String url) =>
+ Future.error(TransportException());
+
+ final flow = AuthorizationCodeGrantManualFlow(
+ clientId,
+ scopes,
+ mockClient(successFullResponse(manual: true), expectClose: false),
+ manualUserPromptError);
+ expect(flow.run(), throwsA(isTransportException));
+ });
+
+ test('transport-exception', () {
+ final flow = AuthorizationCodeGrantManualFlow(
+ clientId, scopes, transportFailure, manualUserPrompt);
+ expect(flow.run(), throwsA(isTransportException));
+ });
+
+ test('invalid-server-response', () {
+ final flow = AuthorizationCodeGrantManualFlow(clientId, scopes,
+ mockClient(invalidResponse, expectClose: false), manualUserPrompt);
+ expect(flow.run(), throwsA(isException));
+ });
+ });
+
+ group('http-server', () {
+ void callRedirectionEndpoint(Uri authCodeCall) {
+ final ioClient = HttpClient();
+ ioClient
+ .getUrl(authCodeCall)
+ .then((request) => request.close())
+ .then((response) => response.drain())
+ .whenComplete(expectAsync0(ioClient.close));
+ }
+
+ void userPrompt(String url) {
+ final redirectUri = validateUserPromptUri(url);
+ final authCodeCall = Uri(
+ scheme: redirectUri.scheme,
+ host: redirectUri.host,
+ port: redirectUri.port,
+ path: redirectUri.path,
+ queryParameters: {
+ 'state': Uri.parse(url).queryParameters['state'],
+ 'code': 'mycode',
+ });
+ callRedirectionEndpoint(authCodeCall);
+ }
+
+ void userPromptInvalidAuthCodeCallback(String url) {
+ final redirectUri = validateUserPromptUri(url);
+ final authCodeCall = Uri(
+ scheme: redirectUri.scheme,
+ host: redirectUri.host,
+ port: redirectUri.port,
+ path: redirectUri.path,
+ queryParameters: {
+ 'state': Uri.parse(url).queryParameters['state'],
+ 'error': 'failed to authenticate',
+ });
+ callRedirectionEndpoint(authCodeCall);
+ }
+
+ test('successfull', () async {
+ final flow = AuthorizationCodeGrantServerFlow(
+ clientId,
+ scopes,
+ mockClient(successFullResponse(manual: false), expectClose: false),
+ expectAsync1(userPrompt));
+ validateAccessCredentials(await flow.run());
+ });
+
+ test('transport-exception', () {
+ final flow = AuthorizationCodeGrantServerFlow(
+ clientId, scopes, transportFailure, expectAsync1(userPrompt));
+ expect(flow.run(), throwsA(isTransportException));
+ });
+
+ test('invalid-server-response', () {
+ final flow = AuthorizationCodeGrantServerFlow(
+ clientId,
+ scopes,
+ mockClient(invalidResponse, expectClose: false),
+ expectAsync1(userPrompt));
+ expect(flow.run(), throwsA(isException));
+ });
+
+ test('failed-authentication', () {
+ final flow = AuthorizationCodeGrantServerFlow(
+ clientId,
+ scopes,
+ mockClient(successFullResponse(manual: false), expectClose: false),
+ expectAsync1(userPromptInvalidAuthCodeCallback));
+ expect(flow.run(), throwsA(isUserConsentException));
+ });
+ }, testOn: '!browser');
+ });
+
+ group('scopes-from-tokeninfo-endpoint', () {
+ final successfulResponseJson = jsonEncode({
+ 'issued_to': 'XYZ.apps.googleusercontent.com',
+ 'audience': 'XYZ.apps.googleusercontent.com',
+ 'scope': 'scopeA scopeB',
+ 'expires_in': 3210,
+ 'access_type': 'offline'
+ });
+ const expectedUri =
+ 'https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=my_token';
+
+ test('successfull', () async {
+ final http = mockClient(expectAsync1((BaseRequest request) async {
+ expect(request.url.toString(), expectedUri);
+ return Response(successfulResponseJson, 200);
+ }), expectClose: false);
+ final scopes = await obtainScopesFromAccessToken('my_token', http);
+ expect(scopes, equals(['scopeA', 'scopeB']));
+ });
+
+ test('non-200-status-code', () {
+ final http = mockClient(expectAsync1((BaseRequest request) async {
+ expect(request.url.toString(), expectedUri);
+ return Response(successfulResponseJson, 201);
+ }), expectClose: false);
+ expect(obtainScopesFromAccessToken('my_token', http), throwsException);
+ });
+
+ test('no-scope', () {
+ final http = mockClient(expectAsync1((BaseRequest request) async {
+ expect(request.url.toString(), expectedUri);
+ return Response(jsonEncode({}), 200);
+ }), expectClose: false);
+ expect(obtainScopesFromAccessToken('my_token', http), throwsException);
+ });
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_force.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_force.js
new file mode 100644
index 0000000..16e58fa
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_force.js
@@ -0,0 +1,85 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() {}
+ GapiAuth.prototype.init = function(doneCallback) {
+ doneCallback();
+ };
+ GapiAuth.prototype.authorize = function(json, doneCallback) {
+ /*
+ Input:
+ argument1 = {
+ 'client_id'
+ 'immediate'
+ 'approval_prompt'
+ 'response_type'
+ 'scope'
+ 'access_type'
+ };
+ argument2 = dartCallback(json);
+
+ Output:
+ output_1 = {
+ 'token_type',
+ 'access_token',
+ 'expires_in',
+ 'code',
+ 'state',
+ 'error',
+ };
+ */
+
+ var client_id = json['client_id'];
+ var immediate = json['immediate'];
+ var approval_prompt = json['approval_prompt'];
+ var response_type = json['response_type'];
+ var scope = json['scope'];
+ var access_type = json['access_type'];
+
+ if (client_id == 'foo_client' &&
+ immediate == false &&
+ approval_prompt == 'force' &&
+ response_type == 'code token' &&
+ scope == 'scope1 scope2' &&
+ access_type == 'offline') {
+ doneCallback({
+ 'token_type' : 'Bearer',
+ 'access_token' : 'foo_token',
+ 'expires_in' : '3210',
+ 'code' : 'mycode'
+ });
+ } else {
+ throw 'error';
+ }
+ };
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_force_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_force_test.dart
new file mode 100644
index 0000000..8633efb
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_force_test.dart
@@ -0,0 +1,39 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+import 'package:googleapis_auth/src/utils.dart' as utils;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_auth_hybrid_force.js');
+
+ test('gapi-auth-hybrid-force-test', () async {
+ final clientId = auth.ClientId('foo_client', 'foo_secret');
+ final scopes = ['scope1', 'scope2'];
+
+ final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
+ final result = await flow.runHybridFlow();
+
+ final credentials = result.credentials;
+
+ final date = DateTime.now().toUtc().add(
+ const Duration(seconds: 3210 - utils.maxExpectedTimeDiffInSeconds));
+ final difference = credentials.accessToken.expiry.difference(date);
+ final seconds = difference.inSeconds;
+
+ expect(-3 <= seconds && seconds <= 3, isTrue);
+ expect(credentials.accessToken.data, 'foo_token');
+ expect(credentials.refreshToken, isNull);
+ expect(credentials.scopes, hasLength(2));
+ expect(credentials.scopes[0], 'scope1');
+ expect(credentials.scopes[1], 'scope2');
+
+ expect(result.authorizationCode, 'mycode');
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_immediate.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_immediate.js
new file mode 100644
index 0000000..57da7e2
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_immediate.js
@@ -0,0 +1,85 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() {}
+ GapiAuth.prototype.init = function(doneCallback) {
+ doneCallback();
+ };
+ GapiAuth.prototype.authorize = function(json, doneCallback) {
+ /*
+ Input:
+ argument1 = {
+ 'client_id'
+ 'immediate'
+ 'approval_prompt'
+ 'response_type'
+ 'scope'
+ 'access_type'
+ };
+ argument2 = dartCallback(json);
+
+ Output:
+ output_1 = {
+ 'token_type',
+ 'access_token',
+ 'expires_in',
+ 'code',
+ 'state',
+ 'error',
+ };
+ */
+
+ var client_id = json['client_id'];
+ var immediate = json['immediate'];
+ var approval_prompt = json['approval_prompt'];
+ var response_type = json['response_type'];
+ var scope = json['scope'];
+ var access_type = json['access_type'];
+
+ if (client_id == 'foo_client' &&
+ immediate == true &&
+ approval_prompt == 'auto' &&
+ response_type == 'code token' &&
+ scope == 'scope1 scope2' &&
+ access_type == 'offline') {
+ doneCallback({
+ 'token_type' : 'Bearer',
+ 'access_token' : 'foo_token',
+ 'expires_in' : '3210',
+ 'code' : 'mycode'
+ });
+ } else {
+ throw 'error';
+ }
+ };
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_immediate_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_immediate_test.dart
new file mode 100644
index 0000000..e30a79c
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_immediate_test.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+import 'package:googleapis_auth/src/utils.dart' as utils;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_auth_hybrid_immediate.js');
+
+ test('gapi-auth-hybrid-immediate-test', () async {
+ final clientId = auth.ClientId('foo_client', 'foo_secret');
+ final scopes = ['scope1', 'scope2'];
+
+ final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
+ final result = await flow.runHybridFlow(force: false, immediate: true);
+ final credentials = result.credentials;
+
+ final date = DateTime.now().toUtc().add(
+ const Duration(seconds: 3210 - utils.maxExpectedTimeDiffInSeconds));
+ final difference = credentials.accessToken.expiry.difference(date);
+ final seconds = difference.inSeconds;
+
+ expect(-3 <= seconds && seconds <= 3, isTrue);
+ expect(credentials.accessToken.data, 'foo_token');
+ expect(credentials.refreshToken, isNull);
+ expect(credentials.scopes, hasLength(2));
+ expect(credentials.scopes[0], 'scope1');
+ expect(credentials.scopes[1], 'scope2');
+
+ expect(result.authorizationCode, 'mycode');
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce.js
new file mode 100644
index 0000000..c459cea
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce.js
@@ -0,0 +1,85 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() {}
+ GapiAuth.prototype.init = function(doneCallback) {
+ doneCallback();
+ };
+ GapiAuth.prototype.authorize = function(json, doneCallback) {
+ /*
+ Input:
+ argument1 = {
+ 'client_id'
+ 'immediate'
+ 'approval_prompt'
+ 'response_type'
+ 'scope'
+ 'access_type'
+ };
+ argument2 = dartCallback(json);
+
+ Output:
+ output_1 = {
+ 'token_type',
+ 'access_token',
+ 'expires_in',
+ 'code',
+ 'state',
+ 'error',
+ };
+ */
+
+ var client_id = json['client_id'];
+ var immediate = json['immediate'];
+ var approval_prompt = json['approval_prompt'];
+ var response_type = json['response_type'];
+ var scope = json['scope'];
+ var access_type = json['access_type'];
+
+ if (client_id == 'foo_client' &&
+ immediate == false &&
+ approval_prompt == 'auto' &&
+ response_type == 'code token' &&
+ scope == 'scope1 scope2' &&
+ access_type == 'offline') {
+ doneCallback({
+ 'token_type' : 'Bearer',
+ 'access_token' : 'foo_token',
+ 'expires_in' : '3210',
+ 'code' : 'mycode'
+ });
+ } else {
+ throw 'error';
+ }
+ };
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce_test.dart
new file mode 100644
index 0000000..aa4fede
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_hybrid_nonforce_test.dart
@@ -0,0 +1,38 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+import 'package:googleapis_auth/src/utils.dart' as utils;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_auth_hybrid_nonforce.js');
+
+ test('gapi-auth-hybrid-nonforce-test', () async {
+ final clientId = auth.ClientId('foo_client', 'foo_secret');
+ final scopes = ['scope1', 'scope2'];
+
+ final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
+ final result = await flow.runHybridFlow(force: false);
+ final credentials = result.credentials;
+
+ final date = DateTime.now().toUtc().add(
+ const Duration(seconds: 3210 - utils.maxExpectedTimeDiffInSeconds));
+ final difference = credentials.accessToken.expiry.difference(date);
+ final seconds = difference.inSeconds;
+
+ expect(-3 <= seconds && seconds <= 3, isTrue);
+ expect(credentials.accessToken.data, 'foo_token');
+ expect(credentials.refreshToken, isNull);
+ expect(credentials.scopes, hasLength(2));
+ expect(credentials.scopes[0], 'scope1');
+ expect(credentials.scopes[1], 'scope2');
+
+ expect(result.authorizationCode, 'mycode');
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_immediate.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_immediate.js
new file mode 100644
index 0000000..24e942d
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_immediate.js
@@ -0,0 +1,84 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() {}
+ GapiAuth.prototype.init = function(doneCallback) {
+ doneCallback();
+ };
+ GapiAuth.prototype.authorize = function(json, doneCallback) {
+ /*
+ Input:
+ argument1 = {
+ 'client_id'
+ 'immediate'
+ 'approval_prompt'
+ 'response_type'
+ 'scope'
+ 'access_type'
+ };
+ argument2 = dartCallback(json);
+
+ Output:
+ output_1 = {
+ 'token_type',
+ 'access_token',
+ 'expires_in',
+ 'code',
+ 'state',
+ 'error',
+ };
+ */
+
+ var client_id = json['client_id'];
+ var immediate = json['immediate'];
+ var approval_prompt = json['approval_prompt'];
+ var response_type = json['response_type'];
+ var scope = json['scope'];
+ var access_type = json['access_type'];
+
+ if (client_id == 'foo_client' &&
+ immediate == true &&
+ approval_prompt == 'auto' &&
+ response_type == 'token' &&
+ scope == 'scope1 scope2' &&
+ access_type == 'online') {
+ doneCallback({
+ 'token_type' : 'Bearer',
+ 'access_token' : 'foo_token',
+ 'expires_in' : '3210'
+ });
+ } else {
+ throw 'error';
+ }
+ };
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_immediate_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_immediate_test.dart
new file mode 100644
index 0000000..b8c9eb6
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_immediate_test.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+import 'package:googleapis_auth/src/utils.dart' as utils;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_auth_immediate.js');
+
+ test('gapi-auth-force', () async {
+ final clientId = auth.ClientId('foo_client', 'foo_secret');
+ final scopes = ['scope1', 'scope2'];
+
+ final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
+ final credentials =
+ await flow.obtainAccessCredentialsViaUserConsent(immediate: true);
+ final date = DateTime.now().toUtc().add(
+ const Duration(seconds: 3210 - utils.maxExpectedTimeDiffInSeconds));
+ final difference = credentials.accessToken.expiry.difference(date);
+ final seconds = difference.inSeconds;
+
+ expect(-3 <= seconds && seconds <= 3, isTrue);
+ expect(credentials.accessToken.data, 'foo_token');
+ expect(credentials.refreshToken, isNull);
+ expect(credentials.scopes, hasLength(2));
+ expect(credentials.scopes[0], 'scope1');
+ expect(credentials.scopes[1], 'scope2');
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_implicit_idtoken.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_implicit_idtoken.js
new file mode 100644
index 0000000..cc01553
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_implicit_idtoken.js
@@ -0,0 +1,86 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function () {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() { }
+ GapiAuth.prototype.init = function (doneCallback) {
+ doneCallback();
+ };
+ GapiAuth.prototype.authorize = function (json, doneCallback) {
+ /*
+ Input:
+ argument1 = {
+ 'client_id'
+ 'immediate'
+ 'approval_prompt'
+ 'response_type'
+ 'scope'
+ 'access_type'
+ };
+ argument2 = dartCallback(json);
+
+ Output:
+ output_1 = {
+ 'token_type',
+ 'access_token',
+ 'expires_in',
+ 'id_token',
+ 'code',
+ 'state',
+ 'error',
+ };
+ */
+
+ var client_id = json['client_id'];
+ var immediate = json['immediate'];
+ var approval_prompt = json['approval_prompt'];
+ var response_type = json['response_type'];
+ var scope = json['scope'];
+ var access_type = json['access_type'];
+
+ if (client_id == 'foo_client' &&
+ immediate == false &&
+ approval_prompt == 'auto' &&
+ response_type == 'id_token token' &&
+ scope == 'scope1 scope2' &&
+ access_type == 'online') {
+ doneCallback({
+ 'token_type': 'Bearer',
+ 'access_token': 'foo_token',
+ 'id_token': 'foo_id_token',
+ 'expires_in': '3210'
+ });
+ } else {
+ throw 'error';
+ }
+ };
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_implicit_idtoken_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_implicit_idtoken_test.dart
new file mode 100644
index 0000000..89f8941
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_implicit_idtoken_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+import 'package:googleapis_auth/src/utils.dart' as utils;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_auth_implicit_idtoken.js');
+
+ test('gapi-auth-implicit-idtoken', () async {
+ final clientId = auth.ClientId('foo_client', 'foo_secret');
+ final scopes = ['scope1', 'scope2'];
+
+ final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
+ final credentials = await flow.obtainAccessCredentialsViaUserConsent(
+ responseTypes: [auth.ResponseType.idToken, auth.ResponseType.token]);
+
+ final date = DateTime.now().toUtc().add(
+ const Duration(seconds: 3210 - utils.maxExpectedTimeDiffInSeconds));
+ final difference = credentials.accessToken.expiry.difference(date);
+ final seconds = difference.inSeconds;
+
+ expect(-3 <= seconds && seconds <= 3, isTrue);
+ expect(credentials.accessToken.data, 'foo_token');
+ expect(credentials.refreshToken, isNull);
+ expect(credentials.scopes, hasLength(2));
+ expect(credentials.scopes[0], 'scope1');
+ expect(credentials.scopes[1], 'scope2');
+ expect(credentials.idToken, 'foo_id_token');
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_nonforce.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_nonforce.js
new file mode 100644
index 0000000..c071126
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_nonforce.js
@@ -0,0 +1,84 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() {}
+ GapiAuth.prototype.init = function(doneCallback) {
+ doneCallback();
+ };
+ GapiAuth.prototype.authorize = function(json, doneCallback) {
+ /*
+ Input:
+ argument1 = {
+ 'client_id'
+ 'immediate'
+ 'approval_prompt'
+ 'response_type'
+ 'scope'
+ 'access_type'
+ };
+ argument2 = dartCallback(json);
+
+ Output:
+ output_1 = {
+ 'token_type',
+ 'access_token',
+ 'expires_in',
+ 'code',
+ 'state',
+ 'error',
+ };
+ */
+
+ var client_id = json['client_id'];
+ var immediate = json['immediate'];
+ var approval_prompt = json['approval_prompt'];
+ var response_type = json['response_type'];
+ var scope = json['scope'];
+ var access_type = json['access_type'];
+
+ if (client_id == 'foo_client' &&
+ immediate == false &&
+ approval_prompt == 'auto' &&
+ response_type == 'token' &&
+ scope == 'scope1 scope2' &&
+ access_type == 'online') {
+ doneCallback({
+ 'token_type' : 'Bearer',
+ 'access_token' : 'foo_token',
+ 'expires_in' : '3210'
+ });
+ } else {
+ throw 'error';
+ }
+ };
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_nonforce_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_nonforce_test.dart
new file mode 100644
index 0000000..7e42428
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_nonforce_test.dart
@@ -0,0 +1,35 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+import 'package:googleapis_auth/src/utils.dart' as utils;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_auth_nonforce.js');
+
+ test('gapi-auth-nonforce', () async {
+ final clientId = auth.ClientId('foo_client', 'foo_secret');
+ final scopes = ['scope1', 'scope2'];
+
+ final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
+ final credentials = await flow.obtainAccessCredentialsViaUserConsent();
+
+ final date = DateTime.now().toUtc().add(
+ const Duration(seconds: 3210 - utils.maxExpectedTimeDiffInSeconds));
+ final difference = credentials.accessToken.expiry.difference(date);
+ final seconds = difference.inSeconds;
+
+ expect(-3 <= seconds && seconds <= 3, isTrue);
+ expect(credentials.accessToken.data, 'foo_token');
+ expect(credentials.refreshToken, isNull);
+ expect(credentials.scopes, hasLength(2));
+ expect(credentials.scopes[0], 'scope1');
+ expect(credentials.scopes[1], 'scope2');
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_user_denied.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_user_denied.js
new file mode 100644
index 0000000..4c27dc4
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_user_denied.js
@@ -0,0 +1,82 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() {}
+ GapiAuth.prototype.init = function(doneCallback) {
+ doneCallback();
+ };
+ GapiAuth.prototype.authorize = function(json, doneCallback) {
+ /*
+ Input:
+ argument1 = {
+ 'client_id'
+ 'immediate'
+ 'approval_prompt'
+ 'response_type'
+ 'scope'
+ 'access_type'
+ };
+ argument2 = dartCallback(json);
+
+ Output:
+ output_1 = {
+ 'token_type',
+ 'access_token',
+ 'expires_in',
+ 'code',
+ 'state',
+ 'error',
+ };
+ */
+
+ var client_id = json['client_id'];
+ var immediate = json['immediate'];
+ var approval_prompt = json['approval_prompt'];
+ var response_type = json['response_type'];
+ var scope = json['scope'];
+ var access_type = json['access_type'];
+
+ if (client_id == 'foo_client' &&
+ immediate == false &&
+ approval_prompt == 'auto' &&
+ response_type == 'token' &&
+ scope == 'scope1 scope2' &&
+ access_type == 'online') {
+ doneCallback({
+ 'error' : 'failed to get user consent',
+ });
+ } else {
+ throw 'error';
+ }
+ };
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
\ No newline at end of file
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_user_denied_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_user_denied_test.dart
new file mode 100644
index 0000000..cf8e45a
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_auth_user_denied_test.dart
@@ -0,0 +1,27 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_auth_user_denied.js');
+
+ test('gapi-auth-user-denied', () async {
+ final clientId = auth.ClientId('foo_client', 'foo_secret');
+ final scopes = ['scope1', 'scope2'];
+
+ final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
+ try {
+ await flow.obtainAccessCredentialsViaUserConsent();
+ fail('expected error');
+ } catch (error) {
+ expect(error is auth.UserConsentException, isTrue);
+ }
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_failure.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_failure.js
new file mode 100644
index 0000000..be1f56c
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_failure.js
@@ -0,0 +1,39 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ function GapiAuth() {}
+ // We do not set the init/authorize functions, which should make the
+ // the initialization fail.
+ // GapiAuth.prototype.init = ...;
+ // GapiAuth.prototype.authorize = ...;
+
+ // Initialize the gapi.auth mock.
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
\ No newline at end of file
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_failure_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_failure_test.dart
new file mode 100644
index 0000000..2dc43dd
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_failure_test.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_initialize_failure.js');
+
+ test('gapi-initialize-failure', () {
+ final clientId = auth.ClientId('a', 'b');
+ final scopes = ['scope1', 'scope2'];
+
+ expect(auth.createImplicitBrowserFlow(clientId, scopes), throwsStateError);
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_successful.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_successful.js
new file mode 100644
index 0000000..308db5b
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_successful.js
@@ -0,0 +1,37 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+(function() {
+ // This function looks up the URL this script was loaded in and finds the
+ // name of the callback function to call when the library is read.
+ // The URL of the script load looks like:
+ // http://localhost:8080/folder/file?onload=dartGapiLoaded
+ function findDartOnLoadCallback() {
+ var scripts = document.getElementsByTagName('script');
+ var self = scripts[scripts.length - 1];
+
+ var equalsSign = self.src.indexOf('=');
+ if (equalsSign <= 0) throw 'error';
+
+ var callbackName = self.src.substring(equalsSign + 1);
+ if (callbackName.length <= 0) throw 'error';
+
+ var dartFunction = window[callbackName];
+ if (dartFunction == null) throw 'error';
+
+ return dartFunction;
+ }
+
+ // Initialize the gapi.auth mock.
+ function GapiAuth() {}
+ GapiAuth.prototype.init = function (dartCallback) {
+ dartCallback();
+ };
+ window.gapi = new Object();
+ window.gapi.auth = new GapiAuth();
+
+ // Call the dart function. This signals that gapi.auth was loaded.
+ var dartFunction = findDartOnLoadCallback();
+ dartFunction();
+})();
\ No newline at end of file
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_successful_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_successful_test.dart
new file mode 100644
index 0000000..b2d8522
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_initialize_successful_test.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+
+import 'utils.dart';
+
+void main() {
+ impl.gapiUrl = resource('gapi_initialize_successful.js');
+
+ test('gapi-initialize-successful', () {
+ final clientId = auth.ClientId('a', 'b');
+ final clientId2 = auth.ClientId('c', 'd');
+ final scopes = ['scope1', 'scope2'];
+
+ expect(auth.createImplicitBrowserFlow(clientId, scopes), completes);
+ expect(auth.createImplicitBrowserFlow(clientId2, scopes), completes);
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_load_failure.js b/googleapis_auth/test/oauth2_flows/implicit/gapi_load_failure.js
new file mode 100644
index 0000000..d83bbba
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_load_failure.js
@@ -0,0 +1,6 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// We do not set 'window.gapi = ...'
+this is a syntax error
\ No newline at end of file
diff --git a/googleapis_auth/test/oauth2_flows/implicit/gapi_load_failure_test.dart b/googleapis_auth/test/oauth2_flows/implicit/gapi_load_failure_test.dart
new file mode 100644
index 0000000..23e77c8
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/gapi_load_failure_test.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('browser')
+import 'dart:html';
+import 'dart:js' as js;
+
+import 'package:test/test.dart';
+import 'package:googleapis_auth/auth_browser.dart' as auth;
+import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
+
+import 'utils.dart';
+
+void main() {
+ // The default timeout is too small for us to detect the timeout of loading
+ // the gapi.auth library.
+ const timeout = Timeout(Duration(hours: 1));
+
+ final clientId = auth.ClientId('a', 'b');
+ final scopes = ['scope1', 'scope2'];
+
+ test('gapi-load-failure', () {
+ impl.gapiUrl = resource('non_existent.js');
+ expect(auth.createImplicitBrowserFlow(clientId, scopes), throwsException);
+ }, timeout: timeout);
+
+ test('gapi-load-failure--syntax-error', () async {
+ impl.gapiUrl = resource('gapi_load_failure.js');
+
+ // Reset test_controller.js's window.onerror registration.
+ // This makes sure we can catch the onError callback when the syntax error
+ // is produced.
+ js.context['onerror'] = null;
+
+ window.onError.listen(expectAsync1((error) {
+ error.preventDefault();
+ }));
+
+ final sw = Stopwatch()..start();
+ try {
+ await auth.createImplicitBrowserFlow(clientId, scopes);
+ fail('expected error');
+ } catch (error) {
+ final elapsed =
+ (sw.elapsed - impl.ImplicitFlow.callbackTimeout).inSeconds;
+ expect(-3 <= elapsed && elapsed <= 3, isTrue);
+ }
+ }, timeout: timeout);
+}
diff --git a/googleapis_auth/test/oauth2_flows/implicit/utils.dart b/googleapis_auth/test/oauth2_flows/implicit/utils.dart
new file mode 100644
index 0000000..a46dc71
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/implicit/utils.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:html';
+
+String resource(String name) =>
+ Uri.parse(document.baseUri!).resolve(name).toString();
diff --git a/googleapis_auth/test/oauth2_flows/jwt_test.dart b/googleapis_auth/test/oauth2_flows/jwt_test.dart
new file mode 100644
index 0000000..ae71de0
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/jwt_test.dart
@@ -0,0 +1,87 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.jwt_test;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:googleapis_auth/src/oauth2_flows/jwt.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+import '../test_utils.dart';
+
+void main() {
+ const tokenUrl = 'https://accounts.google.com/o/oauth2/token';
+
+ Future<Response> successfulSignRequest(Request request) {
+ expect(request.method, equals('POST'));
+ expect(request.url.toString(), equals(tokenUrl));
+
+ // We are not asserting what comes after '&assertion=' because this is
+ // time dependent.
+ expect(
+ request.body,
+ startsWith(
+ 'grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer'
+ '&assertion='));
+ final body = jsonEncode({
+ 'access_token': 'atok',
+ 'expires_in': 3600,
+ 'token_type': 'Bearer',
+ });
+ return Future.value(Response(body, 200));
+ }
+
+ Future<Response> invalidAccessToken(Request request) {
+ final body = jsonEncode({
+ // Missing 'expires_in' entry
+ 'access_token': 'atok',
+ 'token_type': 'Bearer',
+ });
+ return Future.value(Response(body, 200));
+ }
+
+ group('jwt-flow', () {
+ const clientEmail = 'a@b.com';
+ final scopes = ['s1', 's2'];
+
+ test('successfull', () async {
+ final flow = JwtFlow(clientEmail, testPrivateKey, null, scopes,
+ mockClient(expectAsync1(successfulSignRequest), expectClose: false));
+
+ final credentials = await flow.run();
+ expect(credentials.accessToken.data, equals('atok'));
+ expect(credentials.accessToken.type, equals('Bearer'));
+ expect(credentials.scopes, equals(['s1', 's2']));
+ expectExpiryOneHourFromNow(credentials.accessToken);
+ });
+
+ test('successfull-with-user', () async {
+ final flow = JwtFlow(clientEmail, testPrivateKey, 'x@y.com', scopes,
+ mockClient(expectAsync1(successfulSignRequest), expectClose: false));
+
+ final credentials = await flow.run();
+ expect(credentials.accessToken.data, equals('atok'));
+ expect(credentials.accessToken.type, equals('Bearer'));
+ expect(credentials.scopes, equals(['s1', 's2']));
+ expectExpiryOneHourFromNow(credentials.accessToken);
+ });
+
+ test('invalid-server-response', () {
+ final flow = JwtFlow(clientEmail, testPrivateKey, null, scopes,
+ mockClient(expectAsync1(invalidAccessToken), expectClose: false));
+
+ expect(flow.run(), throwsA(isException));
+ });
+
+ test('transport-failure', () {
+ final flow =
+ JwtFlow(clientEmail, testPrivateKey, null, scopes, transportFailure);
+
+ expect(flow.run(), throwsA(isTransportException));
+ });
+ });
+}
diff --git a/googleapis_auth/test/oauth2_flows/metadata_server_test.dart b/googleapis_auth/test/oauth2_flows/metadata_server_test.dart
new file mode 100644
index 0000000..302c4f6
--- /dev/null
+++ b/googleapis_auth/test/oauth2_flows/metadata_server_test.dart
@@ -0,0 +1,122 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library googleapis_auth.metadata_server;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:googleapis_auth/src/oauth2_flows/metadata_server.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+import '../test_utils.dart';
+
+void main() {
+ const apiUrl = 'http://metadata/computeMetadata/v1';
+ const apiHeaderKey = 'Metadata-Flavor';
+ const apiHeaderValue = 'Google';
+ const tokenUrl = '$apiUrl/instance/service-accounts/default/token';
+ const scopesUrl = '$apiUrl/instance/service-accounts/default/scopes';
+
+ Future<Response> successfulAccessToken(Request request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(tokenUrl));
+ expect(request.headers[apiHeaderKey], equals(apiHeaderValue));
+
+ final body = jsonEncode({
+ 'access_token': 'atok',
+ 'expires_in': 3600,
+ 'token_type': 'Bearer',
+ });
+ return Future.value(Response(body, 200));
+ }
+
+ Future<Response> invalidAccessToken(Request request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(tokenUrl));
+ expect(request.headers[apiHeaderKey], equals(apiHeaderValue));
+
+ final body = jsonEncode({
+ // Missing 'expires_in' entry
+ 'access_token': 'atok',
+ 'token_type': 'Bearer',
+ });
+ return Future.value(Response(body, 200));
+ }
+
+ Future<Response> successfulScopes(Request request) {
+ expect(request.method, equals('GET'));
+ expect(request.url.toString(), equals(scopesUrl));
+ expect(request.headers[apiHeaderKey], equals(apiHeaderValue));
+
+ return Future.value(Response('s1\ns2', 200));
+ }
+
+ group('metadata-server-authorization-flow', () {
+ test('successfull', () async {
+ final flow = MetadataServerAuthorizationFlow(mockClient(
+ expectAsync1((request) {
+ final url = request.url.toString();
+ if (url == tokenUrl) {
+ return successfulAccessToken(request);
+ } else if (url == scopesUrl) {
+ return successfulScopes(request);
+ } else {
+ fail('Invalid URL $url (expected: $tokenUrl or $scopesUrl).');
+ }
+ }, count: 2),
+ expectClose: false));
+
+ final credentials = await flow.run();
+ expect(credentials.accessToken.data, equals('atok'));
+ expect(credentials.accessToken.type, equals('Bearer'));
+ expect(credentials.scopes, equals(['s1', 's2']));
+ expectExpiryOneHourFromNow(credentials.accessToken);
+ });
+
+ test('invalid-server-reponse', () {
+ var requestNr = 0;
+ final flow = MetadataServerAuthorizationFlow(mockClient(
+ expectAsync1((request) {
+ if (requestNr++ == 0) {
+ return invalidAccessToken(request);
+ } else {
+ return successfulScopes(request);
+ }
+ }, count: 2),
+ expectClose: false));
+ expect(flow.run(), throwsA(isException));
+ });
+
+ test('token-transport-error', () {
+ var requestNr = 0;
+ final flow = MetadataServerAuthorizationFlow(mockClient(
+ expectAsync1((request) {
+ if (requestNr++ == 0) {
+ return transportFailure.get(Uri.http('failure', ''));
+ } else {
+ return successfulScopes(request);
+ }
+ }, count: 2),
+ expectClose: false));
+ expect(flow.run(), throwsA(isTransportException));
+ });
+
+ test('scopes-transport-error', () {
+ var requestNr = 0;
+ final flow = MetadataServerAuthorizationFlow(mockClient(
+ expectAsync1((request) {
+ if (requestNr++ == 0) {
+ return successfulAccessToken(request);
+ } else {
+ return transportFailure.get(Uri.http('failure', ''));
+ }
+ }, count: 2),
+ expectClose: false));
+ expect(flow.run(), throwsA(isTransportException));
+ });
+ });
+}
diff --git a/googleapis_auth/test/oauth2_test.dart b/googleapis_auth/test/oauth2_test.dart
new file mode 100644
index 0000000..1584898
--- /dev/null
+++ b/googleapis_auth/test/oauth2_test.dart
@@ -0,0 +1,367 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.oauth2_test;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:googleapis_auth/auth.dart';
+import 'package:googleapis_auth/src/http_client_base.dart';
+import 'package:googleapis_auth/src/utils.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+final _defaultResponse = Response('', 500);
+
+Future<Response> _defaultResponseHandler(Request _) async => _defaultResponse;
+
+void main() {
+ test('access-token', () {
+ final expiry = DateTime.now().subtract(const Duration(seconds: 1));
+ final expiryUtc = expiry.toUtc();
+
+ expect(() => AccessToken('foo', 'bar', expiry), throwsArgumentError);
+
+ final token = AccessToken('foo', 'bar', expiryUtc);
+ expect(token.type, equals('foo'));
+ expect(token.data, equals('bar'));
+ expect(token.expiry, equals(expiryUtc));
+ expect(token.hasExpired, isTrue);
+
+ final nonExpiredToken =
+ AccessToken('foo', 'bar', expiryUtc.add(const Duration(days: 1)));
+ expect(nonExpiredToken.hasExpired, isFalse);
+ });
+
+ test('access-credentials', () {
+ final expiry = DateTime.now().add(const Duration(days: 1)).toUtc();
+ final aToken = AccessToken('foo', 'bar', expiry);
+
+ final credentials = AccessCredentials(aToken, 'refresh', ['scope']);
+ expect(credentials.accessToken, equals(aToken));
+ expect(credentials.refreshToken, equals('refresh'));
+ expect(credentials.scopes, equals(['scope']));
+ });
+
+ test('client-id', () {
+ final clientId = ClientId('id', 'secret');
+ expect(clientId.identifier, equals('id'));
+ expect(clientId.secret, equals('secret'));
+ });
+
+ group('service-account-credentials', () {
+ final clientId = ClientId.serviceAccount('id');
+
+ const credentials = {
+ 'private_key_id': '301029',
+ 'private_key': testPrivateKeyString,
+ 'client_email': 'a@b.com',
+ 'client_id': 'myid',
+ 'type': 'service_account'
+ };
+
+ test('from-valid-individual-params', () {
+ final credentials =
+ ServiceAccountCredentials('email', clientId, testPrivateKeyString);
+ expect(credentials.email, equals('email'));
+ expect(credentials.clientId, equals(clientId));
+ expect(credentials.privateKey, equals(testPrivateKeyString));
+ expect(credentials.impersonatedUser, isNull);
+ });
+
+ test('from-valid-individual-params-with-user', () {
+ final credentials = ServiceAccountCredentials(
+ 'email', clientId, testPrivateKeyString,
+ impersonatedUser: 'x@y.com');
+ expect(credentials.email, equals('email'));
+ expect(credentials.clientId, equals(clientId));
+ expect(credentials.privateKey, equals(testPrivateKeyString));
+ expect(credentials.impersonatedUser, equals('x@y.com'));
+ });
+
+ test('from-json-string', () {
+ final credentialsFromJson =
+ ServiceAccountCredentials.fromJson(jsonEncode(credentials));
+ expect(credentialsFromJson.email, equals('a@b.com'));
+ expect(credentialsFromJson.clientId.identifier, equals('myid'));
+ expect(credentialsFromJson.clientId.secret, isNull);
+ expect(credentialsFromJson.privateKey, equals(testPrivateKeyString));
+ expect(credentialsFromJson.impersonatedUser, isNull);
+ });
+
+ test('from-json-string-with-user', () {
+ final credentialsFromJson = ServiceAccountCredentials.fromJson(
+ jsonEncode(credentials),
+ impersonatedUser: 'x@y.com');
+ expect(credentialsFromJson.email, equals('a@b.com'));
+ expect(credentialsFromJson.clientId.identifier, equals('myid'));
+ expect(credentialsFromJson.clientId.secret, isNull);
+ expect(credentialsFromJson.privateKey, equals(testPrivateKeyString));
+ expect(credentialsFromJson.impersonatedUser, equals('x@y.com'));
+ });
+
+ test('from-json-map', () {
+ final credentialsFromJson =
+ ServiceAccountCredentials.fromJson(credentials);
+ expect(credentialsFromJson.email, equals('a@b.com'));
+ expect(credentialsFromJson.clientId.identifier, equals('myid'));
+ expect(credentialsFromJson.clientId.secret, isNull);
+ expect(credentialsFromJson.privateKey, equals(testPrivateKeyString));
+ expect(credentialsFromJson.impersonatedUser, isNull);
+ });
+
+ test('from-json-map-with-user', () {
+ final credentialsFromJson = ServiceAccountCredentials.fromJson(
+ credentials,
+ impersonatedUser: 'x@y.com');
+ expect(credentialsFromJson.email, equals('a@b.com'));
+ expect(credentialsFromJson.clientId.identifier, equals('myid'));
+ expect(credentialsFromJson.clientId.secret, isNull);
+ expect(credentialsFromJson.privateKey, equals(testPrivateKeyString));
+ expect(credentialsFromJson.impersonatedUser, equals('x@y.com'));
+ });
+ });
+
+ group('client-wrappers', () {
+ final clientId = ClientId('id', 'secret');
+ final tomorrow = DateTime.now().add(const Duration(days: 1)).toUtc();
+ final yesterday = DateTime.now().subtract(const Duration(days: 1)).toUtc();
+ final aToken = AccessToken('Bearer', 'bar', tomorrow);
+ final credentials = AccessCredentials(aToken, 'refresh', ['s1', 's2']);
+
+ Future<Response> successfulRefresh(Request request) {
+ expect(request.method, equals('POST'));
+ expect('${request.url}',
+ equals('https://accounts.google.com/o/oauth2/token'));
+ expect(
+ request.body,
+ equals('client_id=id&'
+ 'client_secret=secret&'
+ 'refresh_token=refresh&'
+ 'grant_type=refresh_token'));
+ final body = jsonEncode({
+ 'token_type': 'Bearer',
+ 'access_token': 'atoken',
+ 'expires_in': 3600,
+ });
+
+ return Future.value(Response(body, 200, headers: _jsonContentType));
+ }
+
+ Future<Response> refreshErrorResponse(Request request) {
+ final body = jsonEncode({'error': 'An error occured'});
+ return Future<Response>.value(
+ Response(body, 400, headers: _jsonContentType));
+ }
+
+ Future<Response> serverError(Request request) =>
+ Future<Response>.error(Exception('transport layer exception'));
+
+ test('refreshCredentials-successfull', () async {
+ final newCredentials = await refreshCredentials(clientId, credentials,
+ mockClient(expectAsync1(successfulRefresh), expectClose: false));
+ final expectedResultUtc = DateTime.now()
+ .toUtc()
+ .add(const Duration(seconds: 3600 - maxExpectedTimeDiffInSeconds));
+
+ final accessToken = newCredentials.accessToken;
+ expect(accessToken.type, equals('Bearer'));
+ expect(accessToken.data, equals('atoken'));
+ expect(accessToken.expiry.difference(expectedResultUtc).inSeconds,
+ equals(0));
+
+ expect(newCredentials.refreshToken, equals('refresh'));
+ expect(newCredentials.scopes, equals(['s1', 's2']));
+ });
+
+ test('refreshCredentials-http-error', () async {
+ try {
+ await refreshCredentials(
+ clientId, credentials, mockClient(serverError, expectClose: false));
+ fail('expected error');
+ } catch (error) {
+ expect(
+ error.toString(), equals('Exception: transport layer exception'));
+ }
+ });
+
+ test('refreshCredentials-error-response', () async {
+ try {
+ await refreshCredentials(clientId, credentials,
+ mockClient(refreshErrorResponse, expectClose: false));
+ fail('expected error');
+ } catch (error) {
+ expect(error is RefreshFailedException, isTrue);
+ }
+ });
+
+ group('authenticatedClient', () {
+ final url = Uri.parse('http://www.example.com');
+
+ test('successfull', () async {
+ final client = authenticatedClient(
+ mockClient(expectAsync1((request) {
+ expect(request.method, equals('POST'));
+ expect(request.url, equals(url));
+ expect(request.headers.length, equals(1));
+ expect(request.headers['Authorization'], equals('Bearer bar'));
+
+ return Future.value(Response('', 204));
+ }), expectClose: false),
+ credentials);
+ expect(client.credentials, equals(credentials));
+
+ final response = await client.send(RequestImpl('POST', url));
+ expect(response.statusCode, equals(204));
+ });
+
+ test('access-denied', () {
+ final client = authenticatedClient(
+ mockClient(expectAsync1((request) {
+ expect(request.method, equals('POST'));
+ expect(request.url, equals(url));
+ expect(request.headers.length, equals(1));
+ expect(request.headers['Authorization'], equals('Bearer bar'));
+
+ const headers = {'www-authenticate': 'foobar'};
+ return Future.value(Response('', 401, headers: headers));
+ }), expectClose: false),
+ credentials);
+ expect(client.credentials, equals(credentials));
+
+ expect(client.send(RequestImpl('POST', url)),
+ throwsA(isAccessDeniedException));
+ });
+
+ test('non-bearer-token', () {
+ final aToken = credentials.accessToken;
+ final nonBearerCredentials = AccessCredentials(
+ AccessToken('foobar', aToken.data, aToken.expiry),
+ 'refresh',
+ ['s1', 's2']);
+
+ expect(
+ () => authenticatedClient(
+ mockClient(_defaultResponseHandler, expectClose: false),
+ nonBearerCredentials),
+ throwsA(isArgumentError));
+ });
+ });
+
+ group('autoRefreshingClient', () {
+ final url = Uri.parse('http://www.example.com');
+
+ test('up-to-date', () async {
+ final client = autoRefreshingClient(
+ clientId,
+ credentials,
+ mockClient(
+ expectAsync1((request) => Future.value(Response('', 200))),
+ expectClose: false));
+ expect(client.credentials, equals(credentials));
+
+ final response = await client.send(RequestImpl('POST', url));
+ expect(response.statusCode, equals(200));
+ });
+
+ test('no-refresh-token', () {
+ final credentials = AccessCredentials(
+ AccessToken('Bearer', 'bar', yesterday), null, ['s1', 's2']);
+
+ expect(
+ () => autoRefreshingClient(clientId, credentials,
+ mockClient(_defaultResponseHandler, expectClose: false)),
+ throwsA(isArgumentError));
+ });
+
+ test('refresh-failed', () {
+ final credentials = AccessCredentials(
+ AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1', 's2']);
+
+ final client = autoRefreshingClient(
+ clientId,
+ credentials,
+ mockClient(expectAsync1((request) {
+ // This should be a refresh request.
+ expect(request.headers['foo'], isNull);
+ return refreshErrorResponse(request);
+ }), expectClose: false));
+ expect(client.credentials, equals(credentials));
+
+ final request = RequestImpl('POST', url);
+ request.headers.addAll({'foo': 'bar'});
+ expect(client.send(request), throwsA(isRefreshFailedException));
+ });
+
+ test('invalid-content-type', () {
+ final credentials = AccessCredentials(
+ AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1', 's2']);
+
+ final client = autoRefreshingClient(
+ clientId,
+ credentials,
+ mockClient(expectAsync1((request) {
+ // This should be a refresh request.
+ expect(request.headers['foo'], isNull);
+ final headers = {'content-type': 'image/png'};
+
+ return Future.value(Response('', 200, headers: headers));
+ }), expectClose: false));
+ expect(client.credentials, equals(credentials));
+
+ final request = RequestImpl('POST', url);
+ request.headers.addAll({'foo': 'bar'});
+ expect(client.send(request), throwsA(isException));
+ });
+
+ test('successful-refresh', () async {
+ var serverInvocation = 0;
+
+ final credentials = AccessCredentials(
+ AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1']);
+
+ final client = autoRefreshingClient(
+ clientId,
+ credentials,
+ mockClient(
+ expectAsync1((request) {
+ if (serverInvocation++ == 0) {
+ // This should be a refresh request.
+ expect(request.headers['foo'], isNull);
+ return successfulRefresh(request);
+ } else {
+ // This is the real request.
+ expect(request.headers['foo'], equals('bar'));
+ return Future.value(Response('', 200));
+ }
+ }, count: 2),
+ expectClose: false));
+ expect(client.credentials, equals(credentials));
+
+ var executed = false;
+ client.credentialUpdates.listen(expectAsync1((newCredentials) {
+ expect(newCredentials.accessToken.type, equals('Bearer'));
+ expect(newCredentials.accessToken.data, equals('atoken'));
+ executed = true;
+ }), onDone: expectAsync0(() {}));
+
+ final request = RequestImpl('POST', url);
+ request.headers.addAll({'foo': 'bar'});
+
+ final response = await client.send(request);
+ expect(response.statusCode, equals(200));
+
+ // The `client.send()` will have triggered a credentials refresh.
+ expect(executed, isTrue);
+
+ client.close();
+ });
+ });
+ });
+}
+
+const _jsonContentType = {'content-type': 'application/json'};
diff --git a/googleapis_auth/test/test_utils.dart b/googleapis_auth/test/test_utils.dart
new file mode 100644
index 0000000..e067a96
--- /dev/null
+++ b/googleapis_auth/test/test_utils.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library googleapis_auth.test_utils;
+
+import 'dart:async';
+
+import 'package:googleapis_auth/auth.dart';
+import 'package:googleapis_auth/src/crypto/pem.dart';
+import 'package:googleapis_auth/src/utils.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
+import 'package:test/test.dart';
+
+const Matcher isUserConsentException = TypeMatcher<UserConsentException>();
+
+const Matcher isRefreshFailedException = TypeMatcher<RefreshFailedException>();
+
+const Matcher isAccessDeniedException = TypeMatcher<AccessDeniedException>();
+
+const Matcher isTransportException = TypeMatcher<TransportException>();
+
+class TransportException implements Exception {}
+
+Client get transportFailure => MockClient(
+ expectAsync1((Request _) => Future<Response>.error(TransportException())));
+
+const testPrivateKeyString = '''-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAuDOwXO14ltE1j2O0iDSuqtbw/1kMKjeiki3oehk2zNoUte42
+/s2rX15nYCkKtYG/r8WYvKzb31P4Uow1S4fFydKNWxgX4VtEjHgeqfPxeCL9wiJc
+9KkEt4fyhj1Jo7193gCLtovLAFwPzAMbFLiXWkfqalJ5Z77fOE4Mo7u4pEgxNPgL
+VFGe0cEOAsHsKlsze+m1pmPHwWNVTcoKe5o0hOzy6hCPgVc6me6Y7aO8Fb4OVg0l
+XQdQpWn2ikVBpzBcZ6InnYyJ/CJNa3WL1LJ65mmYnfHtKGoMqhLK48OReguwRwwF
+e9/2+8UcdZcN5rsvt7yg3ZrKNH8rx+wZ36sRewIDAQABAoIBAQCn1HCcOsHkqDlk
+rDOQ5m8+uRhbj4bF8GrvRWTL2q1TeF/mY2U4Q6wg+KK3uq1HMzCzthWz0suCb7+R
+dq4YY1ySxoSEuy8G5WFPmyJVNy6Lh1Yty6FmSZlCn1sZdD3kMoK8A0NIz5Xmffrm
+pu3Fs2ozl9K9jOeQ3xgC9RoPFLrm8lHJ45Vn+SnTxZnsXT6pwpg3TnFIx5ZinU8k
+l0Um1n80qD2QQDakQ5jyr2odAELLBDlyCkxAglBXAVt4nk9Kl6nxb4snd9dnrL70
+WjLynWQsDczaV9TZIl2hYkMud+9OLVlUUtB+0c5b0p2t2P0sLltDaq3H6pT6yu2G
+8E86J9IBAoGBAPJaTNV5ysVOFn+YwWwRztzrvNArUJkVq8abN0gGp3gUvDEZnvzK
+weF7+lfZzcwVRmQkL3mWLzzZvCx77RfulAzLi5iFuRBPhhhxAPDiDuyL9B7O81G/
+M/W5DPctGOyD/9cnLuh72oij0unc5MLSfzJf8wblpcjJnPBDqIVh6Qt9AoGBAMKT
+Gacf4iSj1xW+0wrnbZlDuyCl6Msptj8ePcvLQrFqQmBwsXmWgVR+gFc/1G3lRft0
+QC6chsmafQHIIPpaDjq3sQ01/tUu7LXL+g/Hw9XtUHbkg3sZIQBtC26rKdStfHNS
+KTvuCgn/dAJNjiohfhWMt9R4Q6E5FV6PqQHJzPJXAoGAC41qZDKuC8GxKNvrPG+M
+4NML6RBngySZT5pOhExs5zh10BFclshDfbAfOtjTCotpE5T1/mG+VrQ6WBSANMfW
+ntWFDfwx2ikwRzH7zX+5HmV9eYp75sWqgGgVyiKIMZ4JMARaJBLjU+gbQbKZ5P+L
+uKcCOq3vvSZ/KKTQ/6qvJTECgYBiWgbCgoxF5wdmd4Gn5llw+lqRYyur3hbACuJD
+rCe3FDYfF3euNRSEiDkJYTtYnWbldtqmdPpw14VOrEF3KqQ8q/Nz8RIx4jlGn6dz
+6I8mCIH+xv1q8MXMuFHqC9zmIxdgF2y+XVF3wkd6jodI5oscC3g0juHokbkqhkVw
+oPfWmwKBgBfR6jv0gWWeWTfkNwj+cMLHQV1uvz6JyLH5K4iISEDFxYkd37jrHB8A
+9hz9UDfmCbSs2j8CXDg7zCayM6tfu4Vtx+8S5g3oN6sa1JXFY1Os7SoXhTfX9M+7
+QpYYDJZwkgZrVQoKMIdCs9xfyVhZERq945NYLekwE1t2W+tOVBgR
+-----END RSA PRIVATE KEY-----''';
+
+final testPrivateKey = keyFromString(testPrivateKeyString);
+
+void expectExpiryOneHourFromNow(AccessToken accessToken) {
+ final now = DateTime.now().toUtc();
+ final diff = accessToken.expiry.difference(now).inSeconds -
+ (3600 - maxExpectedTimeDiffInSeconds);
+ expect(-2 <= diff && diff <= 2, isTrue);
+}
+
+Client mockClient(Future<Response> Function(Request _) requestHandler,
+ {bool expectClose = true}) =>
+ ExpectCloseMockClient(requestHandler, expectClose ? 1 : 0);
+
+/// A client which will keep the VM alive until `close()` was called.
+class ExpectCloseMockClient extends MockClient {
+ late Function _expectedToBeCalled;
+
+ ExpectCloseMockClient(
+ Future<Response> Function(Request _) requestHandler, int c)
+ : super(requestHandler) {
+ _expectedToBeCalled = expectAsync0(() {}, count: c);
+ }
+
+ @override
+ void close() {
+ super.close();
+ _expectedToBeCalled();
+ }
+}