refactor: remove fork of googleauth_apis (#1760)

This commit is contained in:
Bryan Oltman
2024-02-28 15:15:27 -05:00
committed by GitHub
parent 523719d1db
commit 3822b5f2a0
81 changed files with 66 additions and 6713 deletions
+1 -44
View File
@@ -21,7 +21,6 @@ jobs:
outputs:
needs_dart_build: ${{ steps.needs_dart_build.outputs.changes }}
needs_redis_build: ${{ steps.needs_redis_build.outputs.changes }}
needs_third_party_build: ${{ steps.needs_third_party_build.outputs.changes }}
needs_verify: ${{ steps.needs_verify.outputs.changes }}
name: 👀 Detect Changes
@@ -52,7 +51,6 @@ jobs:
- packages/shorebird_cli/**
- packages/shorebird_code_push_client/**
- packages/shorebird_code_push_protocol/**
- third_party/googleapis_auth/**
shorebird_code_push_client:
- ./.github/codecov.yml
- ./.github/workflows/main.yaml
@@ -86,17 +84,6 @@ jobs:
- ./.github/actions/dart_package/action.yaml
- packages/redis_client/**
- uses: dorny/paths-filter@v3
name: Redis Detection
id: needs_third_party_build
with:
filters: |
googleapis_auth:
- ./.github/codecov.yml
- ./.github/workflows/main.yaml
- ./.github/actions/dart_package/action.yaml
- third_party/googleapis_auth/**
- uses: dorny/paths-filter@v3
name: Verify Detection
id: needs_verify
@@ -134,30 +121,6 @@ jobs:
with:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
working_directory: packages/${{ matrix.package }}
min_coverage: 99
build_third_party_packages:
needs: changes
if: ${{ needs.changes.outputs.needs_third_party_build != '[]' }}
strategy:
matrix:
package: ${{ fromJSON(needs.changes.outputs.needs_third_party_build) }}
runs-on: ubuntu-latest
name: 🎯 Build ${{ matrix.package }}
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v4
- name: 🎯 Build ${{ matrix.package }}
uses: ./.github/actions/dart_package
with:
codecov_token: ${{ secrets.CODECOV_TOKEN }}
working_directory: third_party/${{ matrix.package }}
min_coverage: 85
build_redis:
needs: changes
@@ -211,13 +174,7 @@ jobs:
ci:
needs:
[
semantic_pull_request,
build_dart_packages,
build_redis,
build_third_party_packages,
verify_packages,
]
[semantic_pull_request, build_dart_packages, build_redis, verify_packages]
if: ${{ always() }}
runs-on: ubuntu-latest
+11 -11
View File
@@ -37,19 +37,19 @@ const microsoftJwtIssuerPrefix = 'https://login.microsoftonline.com/';
const shorebirdTokenEnvVar = 'SHOREBIRD_TOKEN';
typedef ObtainAccessCredentials = Future<oauth2.AccessCredentials> Function(
oauth2.AuthEndpoints authEndpoints,
oauth2.ClientId clientId,
List<String> scopes,
http.Client client,
void Function(String) userPrompt,
);
void Function(String) userPrompt, {
oauth2.AuthEndpoints authEndpoints,
});
typedef RefreshCredentials = Future<oauth2.AccessCredentials> Function(
oauth2.AuthEndpoints authEndpoints,
oauth2.ClientId clientId,
oauth2.AccessCredentials credentials,
http.Client client,
);
http.Client client, {
oauth2.AuthEndpoints authEndpoints,
});
typedef OnRefreshCredentials = void Function(
oauth2.AccessCredentials credentials,
@@ -105,7 +105,6 @@ class AuthenticatedClient extends http.BaseClient {
if (credentials == null) {
final token = _token!;
credentials = _credentials = await _refreshCredentials(
token.authProvider.authEndpoints,
token.authProvider.clientId,
oauth2.AccessCredentials(
// This isn't relevant for a refresh operation.
@@ -114,6 +113,7 @@ class AuthenticatedClient extends http.BaseClient {
token.authProvider.scopes,
),
_baseClient,
authEndpoints: token.authProvider.authEndpoints,
);
_onRefreshCredentials?.call(credentials);
}
@@ -123,10 +123,10 @@ class AuthenticatedClient extends http.BaseClient {
final authProvider = jwt.authProvider;
credentials = _credentials = await _refreshCredentials(
authProvider.authEndpoints,
authProvider.clientId,
credentials,
_baseClient,
authEndpoints: authProvider.authEndpoints,
);
_onRefreshCredentials?.call(credentials);
}
@@ -190,11 +190,11 @@ class Auth {
final client = http.Client();
try {
final credentials = await _obtainAccessCredentials(
authProvider.authEndpoints,
authProvider.clientId,
authProvider.scopes,
client,
prompt,
authEndpoints: authProvider.authEndpoints,
);
final codePushClient = _buildCodePushClient(
@@ -231,11 +231,11 @@ class Auth {
final client = http.Client();
try {
_credentials = await _obtainAccessCredentials(
authProvider.authEndpoints,
authProvider.clientId,
authProvider.scopes,
client,
prompt,
authEndpoints: authProvider.authEndpoints,
);
final codePushClient = _buildCodePushClient(httpClient: this.client);
@@ -368,7 +368,7 @@ extension OauthAuthProvider on Jwt {
extension OauthValues on AuthProvider {
oauth2.AuthEndpoints get authEndpoints => switch (this) {
(AuthProvider.google) => oauth2.GoogleAuthEndpoints(),
(AuthProvider.google) => const oauth2.GoogleAuthEndpoints(),
(AuthProvider.microsoft) => MicrosoftAuthEndpoints(),
};
+17 -8
View File
@@ -281,13 +281,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
google_identity_services_web:
dependency: transitive
description:
name: google_identity_services_web
sha256: "972ff30eebf6a5eab28be3e1e47a45df087ed64d5aefdac0df47758ecdec5385"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
googleapis_auth:
dependency: "direct main"
description:
path: "../../third_party/googleapis_auth"
relative: true
source: path
version: "1.4.1"
name: googleapis_auth
sha256: cafc46446574fd42826aa4cd4d623c94482598fda0a5a5649bf2781bcbc09258
url: "https://pub.dev"
source: hosted
version: "1.5.0"
graphs:
dependency: transitive
description:
@@ -348,10 +357,10 @@ packages:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
source: hosted
version: "0.6.7"
version: "0.7.1"
json_annotation:
dependency: "direct main"
description:
@@ -752,10 +761,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246
sha256: e7d5ecd604e499358c5fe35ee828c0298a320d54455e791e9dcf73486bc8d9f0
url: "https://pub.dev"
source: hosted
version: "14.0.0"
version: "14.1.0"
watcher:
dependency: transitive
description:
+1 -2
View File
@@ -17,8 +17,7 @@ dependencies:
cli_util: ^0.4.0
collection: ^1.17.1
crypto: ^3.0.2
googleapis_auth:
path: ../../third_party/googleapis_auth
googleapis_auth: ^1.5.0
http: ^1.0.0
intl: ^0.19.0
io: ^1.0.4
@@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io' hide Platform;
import 'package:cli_util/cli_util.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:googleapis_auth/googleapis_auth.dart' as oauth2;
import 'package:http/http.dart' as http;
import 'package:jwt/jwt.dart' show Jwt, JwtPayload;
@@ -168,8 +169,13 @@ void main() {
buildCodePushClient: ({Uri? hostedUri, http.Client? httpClient}) {
return codePushClient;
},
obtainAccessCredentials:
(authEndpoints, clientId, scopes, client, userPrompt) async {
obtainAccessCredentials: (
clientId,
scopes,
client,
userPrompt, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async {
return accessCredentials;
},
),
@@ -208,9 +214,13 @@ void main() {
() => AuthenticatedClient.token(
token: ciToken,
httpClient: httpClient,
refreshCredentials:
(authEndpoints, clientId, credentials, client) async =>
accessCredentials,
refreshCredentials: (
clientId,
credentials,
client, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async =>
accessCredentials,
),
returnsNormally,
);
@@ -231,9 +241,13 @@ void main() {
token: ciToken,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials:
(authEndpoints, clientId, credentials, client) async =>
accessCredentials,
refreshCredentials: (
clientId,
credentials,
client, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async =>
accessCredentials,
);
await runWithOverrides(
@@ -265,9 +279,13 @@ void main() {
token: ciToken,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials:
(authEndpoints, clientId, credentials, client) async =>
accessCredentials,
refreshCredentials: (
clientId,
credentials,
client, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async =>
accessCredentials,
);
await runWithOverrides(
@@ -315,9 +333,13 @@ void main() {
credentials: expiredCredentials,
httpClient: httpClient,
onRefreshCredentials: onRefreshCredentialsCalls.add,
refreshCredentials:
(authEndpoints, clientId, credentials, client) async =>
accessCredentials,
refreshCredentials: (
clientId,
credentials,
client, {
AuthEndpoints authEndpoints = const GoogleAuthEndpoints(),
}) async =>
accessCredentials,
);
await runWithOverrides(
-8
View File
@@ -1,8 +0,0 @@
.dart_tool/
.packages
pubspec.lock
# User files
test_integration/*.json
test_integration/client_id*.yaml
test_integration/config.yaml
-193
View File
@@ -1,193 +0,0 @@
## 1.4.1
- Require Dart 2.19 or later.
- Allow latest `package:http`.
## 1.4.0
- Update `README` to include a warning about Flutter application usage.
- Require Dart 2.17 or later.
#### `googlapis_auth.dart`
- `authenticatedClient` function: added optional `bool closeUnderlyingClient`
parameter.
#### `auth_browser.dart` library
- Added `AuthenticationException` and use it instead of `Exception` or
`StateError` in many cases where authentication can fail.
- Added `requestAccessCredentials`, `requestAuthorizationCode`, `revokeConsent`,
and `CodeResponse` to support the new
[Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/overview).
- Deprecated `createImplicitBrowserFlow` function.
#### `auth_io.dart` library
- Added an optional `listenPort` parameter to `clientViaUserConsent`
and `obtainAccessCredentialsViaUserConsent`.
## 1.3.1
- Include `plugin_name` during browser authorization.
## 1.3.0
- The `secret` param in `ClientId` constructor is now optional.
- Use the latest supported Google OAuth 2.0 URL
- `auth_browser` library:
- Migrated to newer `auth2` Javascript API.
- Added support for `hostedDomain` to all applicable functions.
- `createImplicitBrowserFlow`: added (unsupported) `enableDebugLogs` param.
(Maybe helpful for debugging, but should not be used in production.)
- `auth_io` library:
- Generate a longer, secure random state token.
- Implement code verifier logic for the desktop auth flows. See
https://developers.google.com/identity/protocols/oauth2/native-app#create-code-challenge
- `obtainAccessCredentialsViaCodeExchange`
- `scopes` are now acquired from the initial API call and not via a separate
API call to the `tokeninfo` endpoint.
- Added optional `codeVerifier` parameter.
## 1.2.0
- Added an optional `hostedDomain` parameter to many functions in
`auth_io.dart`. If provided, restricts sign-in to Google Apps hosted accounts
at that domain.
- Fix an error when doing OAUTH code exchanged with an undefined secret.
- `clientViaApiKey` is now exported from `googleapis_auth.dart`.
- Added `String? details` to `UserConsentException`.
- Update the host used to access metadata on Google Cloud. From
`http://metadata/` to `http://metadata.google.internal`.
- Require Dart 2.13
- Deprecated `RefreshFailedException` - `ServerRequestFailedException` is used
instead.
## 1.1.0
- Added the `googleapis_auth.dart` library. It is convention to have the default
library within a package align with the package name. `auth.dart` is now
deprecated and will be removed in v2.
- Added `fromJson` factory and `toJson` method to `AccessToken`,
`AccessCredentials`, and `ClientId`.
- Remove dynamic function invocations.
## 1.0.0
- Add support for null-safety.
- Require Dart 2.12 or later.
## 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.
-27
View File
@@ -1,27 +0,0 @@
Copyright 2014, the Dart project authors.
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 LLC 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.
-331
View File
@@ -1,331 +0,0 @@
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
> Do _**NOT**_ use this package (`package:googleapis_auth`) with a
> [Flutter](https://flutter.dev/) application.
>
> Use
> [package:extension_google_sign_in_as_googleapis_auth](https://pub.dev/packages/extension_google_sign_in_as_googleapis_auth)
> instead.
### 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';
// Initialize the browser oauth2 flow functionality then use it to obtain credentials.
Future<AccessCredentials> obtainCredentials() async {
final flow = await createImplicitBrowserFlow(
ClientId('....apps.googleusercontent.com'),
['scope1', 'scope2'],
);
try {
return await flow.obtainAccessCredentialsViaUserConsent();
} finally {
flow.close();
}
}
```
or obtain an authenticated HTTP client via
```dart
import 'package:googleapis_auth/auth_browser.dart';
// Initialize the browser oauth2 flow functionality then use it to
// get an authenticated and auto refreshing client.
Future<AuthClient> obtainAuthenticatedClient() async {
final flow = await createImplicitBrowserFlow(
ClientId('....apps.googleusercontent.com'),
['scope1', 'scope2'],
);
try {
return await flow.clientViaUserConsent();
} finally {
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:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
// Use the oauth2 authentication code flow functionality to obtain
// credentials. [prompt] is used for directing the user to a URI.
Future<AccessCredentials> obtainCredentials() async {
final client = http.Client();
try {
return await obtainAccessCredentialsViaUserConsent(
ClientId('....apps.googleusercontent.com', '...'),
['scope1', 'scope2'],
client,
_prompt,
);
} finally {
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';
// Use the oauth2 code grant server flow functionality to
// get an authenticated and auto refreshing client.
Future<AuthClient> obtainCredentials() async => await clientViaUserConsent(
ClientId('....apps.googleusercontent.com', '...'),
['scope1', 'scope2'],
_prompt,
);
void _prompt(String url) {
print('Please go to the following URL and grant access:');
print(' => $url');
print('');
}
```
The Client ID must be created with a `client_secret` here, however there is no
way to properly secure a `client_secret` for installed/console applications.
Fortunately the OAuth2 flow used in this case
[assumes that the app cannot keep secrets](https://developers.google.com/identity/protocols/oauth2/native-app)
so this particular `client_secret` does not need to be kept secret. You should
however make sure not to re-use the same `client_secret` anywhere secrecy is
required.
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 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:googleapis_auth/auth_io.dart";
import "package:http/http.dart" as http;
...
// Use service account credentials to obtain oauth credentials.
Future<AccessCredentials> obtainCredentials() async {
var accountCredentials = 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 = http.Client();
AccessCredentials credentials =
await obtainAccessCredentialsViaServiceAccount(accountCredentials, scopes, client);
client.close();
return credentials;
}
```
or an authenticated HTTP client via
```dart
import "package:googleapis_auth/auth_io.dart";
...
// Use service account credentials to get an authenticated and auto refreshing client.
Future<AuthClient> obtainAuthenticatedClient() async {
final accountCredentials = 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 = [...];
AuthClient client = await clientViaServiceAccount(accountCredentials, scopes);
return client; // Remember to close the client when you are finished with it.
}
```
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://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using)
for further information.
Here is an example of using the metadata service for obtaining access
credentials on a ComputeEngine VM.
```dart
import "package:googleapis_auth/auth_io.dart";
import "package:http/http.dart" as http;
...
// Use the metadata service to obtain oauth credentials.
Future<AccessCredentials> obtainCredentials() async {
var client = http.Client();
AccessCredentials credentials =
await obtainAccessCredentialsViaMetadataServer(client);
client.close();
return credentials;
}
```
or an authenticated HTTP client via
```dart
import "package:googleapis_auth/auth_io.dart";
...
// Use the metadata service to get an authenticated and auto refreshing client.
Future<AuthClient> obtainAuthenticatedClient() async {
AuthClient client = await clientViaMetadataServer();
return client; // Remember to close the client when you are finished with it.
}
```
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.
An 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/identity/protocols/oauth2)
- [OAuth2 Playground](https://developers.google.com/oauthplayground/)
-11
View File
@@ -1,11 +0,0 @@
// Copyright (c) 2021, 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: comment_references
/// This library has been deprecated. Use [googleapis_auth] instead.
@Deprecated('Use `googleapis_auth.dart` instead')
library auth;
export 'googleapis_auth.dart';
-339
View File
@@ -1,339 +0,0 @@
// 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:async';
import 'package:http/browser_client.dart';
import 'package:http/http.dart';
import 'src/auth_functions.dart';
import 'src/auth_http_utils.dart';
import 'src/http_client_base.dart';
import 'src/oauth2_flows/implicit.dart';
import 'src/oauth2_flows/token_model.dart';
import 'src/service_account_credentials.dart';
export 'googleapis_auth.dart';
export 'src/authentication_exception.dart' show AuthenticationException;
export 'src/oauth2_flows/token_model.dart'
show
CodeResponse,
requestAccessCredentials,
requestAuthorizationCode,
revokeConsent;
/// Will create and complete with a [BrowserOAuth2Flow] object.
///
/// {@template googleapis_auth_gis_deprecated}
/// This member is *deprecated*. Use [requestAccessCredentials] or
/// [requestAuthorizationCode] instead.
/// {@endtemplate}
///
/// 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.
///
/// {@macro googleapis_auth_clientId_param}
///
/// {@template googleapis_auth_baseClient_param}
/// If [baseClient] is provided, all HTTP requests will be made with it.
/// Otherwise, a new [Client] instance will be created.
/// {@endtemplate}
///
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
@Deprecated(
'This member is deprecated. Use requestAccessCredentials or '
'requestAuthorizationCode instead.',
)
Future<BrowserOAuth2Flow> createImplicitBrowserFlow(
ClientId clientId,
List<String> scopes, {
Client? baseClient,
@Deprecated(
'Undocumented feature. May help debugging. '
'Do not include in production code.',
)
bool enableDebugLogs = false,
}) async {
final refCountedClient = baseClient == null
? RefCountedClient(BrowserClient())
: RefCountedClient(baseClient, initialRefCount: 2);
final flow = ImplicitFlow(clientId.identifier, scopes, enableDebugLogs);
try {
await flow.initialize();
} catch (_) {
refCountedClient.close();
rethrow;
}
return BrowserOAuth2Flow._(flow, refCountedClient);
}
/// Used for obtaining oauth2 access credentials.
///
/// {@macro googleapis_auth_gis_deprecated}
///
/// 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.
@Deprecated(
'This member is deprecated. Use requestAccessCredentials or '
'requestAuthorizationCode instead.',
)
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].
///
/// {@template googleapis_auth_force}
/// 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, they 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.
/// {@endtemplate}
///
/// {@template googleapis_auth_immediate}
/// 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 not
/// already logged in) and might get asked to grant the application access
/// (if the application hasn't been granted access before).
/// {@endtemplate}
///
/// {@template googleapis_auth_loginHint}
/// 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.
/// {@endtemplate}
///
/// {@macro googleapis_auth_hostedDomain_param}
///
/// 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.
///
/// {@template googleapis_auth_user_consent_return}
/// The returned [Future] will complete with [AccessCredentials] if the user
/// has given the application access to their data.
/// Otherwise, a [UserConsentException] will be thrown.
/// {@endtemplate}
///
/// {@macro googleapis_auth_hostedDomain_param}
Future<AccessCredentials> obtainAccessCredentialsViaUserConsent({
bool force = false,
bool immediate = false,
String? loginHint,
List<ResponseType>? responseTypes,
String? hostedDomain,
}) {
_ensureOpen();
return _flow.login(
prompt: _promptFromBooleans(force, immediate),
loginHint: loginHint,
responseTypes: responseTypes,
hostedDomain: hostedDomain,
);
}
/// Obtains [AccessCredentials] and returns an authenticated HTTP client.
///
/// {@template googleapis_auth_returned_auto_refresh_client}
/// HTTP requests made on the returned client will get an additional
/// `Authorization` header with the [AccessCredentials] obtained.
/// Once the [AccessCredentials] expire, it will use it's refresh token
/// (if available) to obtain new credentials.
/// See [autoRefreshingClient] for more information.
/// {@endtemplate}
///
/// 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.
///
/// {@macro googleapis_auth_immediate}
///
/// {@macro googleapis_auth_close_the_client}
///
/// {@macro googleapis_auth_loginHint}
///
/// {@macro googleapis_auth_hostedDomain_param}
Future<AutoRefreshingAuthClient> clientViaUserConsent({
bool immediate = false,
String? loginHint,
String? hostedDomain,
}) async {
final credentials = await obtainAccessCredentialsViaUserConsent(
immediate: immediate,
loginHint: loginHint,
hostedDomain: hostedDomain,
);
return _clientFromCredentials(credentials);
}
/// 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.
///
/// {@macro googleapis_auth_force}
///
/// {@macro googleapis_auth_immediate}
///
/// {@macro googleapis_auth_loginHint}
///
/// {@macro googleapis_auth_hostedDomain_param}
Future<HybridFlowResult> runHybridFlow({
bool force = true,
bool immediate = false,
String? loginHint,
String? hostedDomain,
}) async {
_ensureOpen();
final result = await _flow.loginHybrid(
prompt: _promptFromBooleans(force, immediate),
hostedDomain: hostedDomain,
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.
///
/// {@macro googleapis_auth_gis_deprecated}
///
/// 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.
@Deprecated(
'This member is deprecated. Use requestAccessCredentials or '
'requestAuthorizationCode instead.',
)
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(super.client, this.credentials, this._flow)
: _authClient = authenticatedClient(client, credentials);
@override
Future<StreamedResponse> send(BaseRequest request) async {
if (!credentials.accessToken.hasExpired) {
return _authClient.send(request);
}
credentials = await _flow.login(prompt: 'none');
notifyAboutNewCredentials(credentials);
_authClient = authenticatedClient(baseClient, credentials);
return _authClient.send(request);
}
}
String? _promptFromBooleans(bool force, bool immediate) {
if (force) {
if (immediate) {
throw ArgumentError.value(
immediate,
'immediate',
'Cannot be true if `force` is also true.',
);
}
return 'consent';
}
if (immediate) {
return 'none';
}
return null;
}
-207
View File
@@ -1,207 +0,0 @@
// 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 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart';
import 'src/auth_http_utils.dart';
import 'src/oauth2_flows/authorization_code_grant_manual_flow.dart';
import 'src/oauth2_flows/authorization_code_grant_server_flow.dart';
export 'googleapis_auth.dart';
export 'src/metadata_server_client.dart';
export 'src/oauth2_flows/auth_code.dart'
show obtainAccessCredentialsViaCodeExchange;
export 'src/service_account_client.dart';
export 'src/typedefs.dart';
/// Obtains oauth2 credentials and returns an authenticated HTTP client.
///
/// See [obtainAccessCredentialsViaUserConsent] for specifics about the
/// arguments used for obtaining access credentials.
///
/// {@macro googleapis_auth_clientId_param}
///
/// {@macro googleapis_auth_returned_auto_refresh_client}
///
/// {@macro googleapis_auth_baseClient_param}
///
/// {@template googleapis_auth_hostedDomain_param}
/// If provided, restricts sign-in to Google Apps hosted accounts at
/// [hostedDomain]. For more details, see
/// https://developers.google.com/identity/protocols/oauth2/openid-connect#hd-param
/// {@endtemplate}
///
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
/// {@macro googleapis_auth_listen_port}
Future<AutoRefreshingAuthClient> clientViaUserConsent(
AuthEndpoints authEndpoints,
ClientId clientId,
List<String> scopes,
PromptUserForConsent userPrompt, {
Client? baseClient,
String? hostedDomain,
int listenPort = 0,
}) async {
var closeUnderlyingClient = false;
if (baseClient == null) {
baseClient = Client();
closeUnderlyingClient = true;
}
final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
baseClient,
userPrompt,
hostedDomain: hostedDomain,
listenPort: listenPort,
);
AccessCredentials credentials;
try {
credentials = await flow.run();
} catch (e) {
if (closeUnderlyingClient) {
baseClient.close();
}
rethrow;
}
return AutoRefreshingClient(
baseClient,
authEndpoints,
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.
///
/// {@macro googleapis_auth_clientId_param}
///
/// {@macro googleapis_auth_returned_auto_refresh_client}
///
/// {@macro googleapis_auth_baseClient_param}
///
/// {@macro googleapis_auth_hostedDomain_param}
///
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
Future<AutoRefreshingAuthClient> clientViaUserConsentManual(
AuthEndpoints authEndpoints,
ClientId clientId,
List<String> scopes,
PromptUserForConsentManual userPrompt, {
Client? baseClient,
String? hostedDomain,
}) async {
var closeUnderlyingClient = false;
if (baseClient == null) {
baseClient = Client();
closeUnderlyingClient = true;
}
final flow = AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
baseClient,
userPrompt,
hostedDomain: hostedDomain,
);
AccessCredentials credentials;
try {
credentials = await flow.run();
} catch (e) {
if (closeUnderlyingClient) {
baseClient.close();
}
rethrow;
}
return AutoRefreshingClient(
baseClient,
authEndpoints,
clientId,
credentials,
closeUnderlyingClient: closeUnderlyingClient,
);
}
/// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow.
///
/// {@macro googleapis_auth_clientId_param}
///
/// [userPrompt] will be used for directing the user/user-agent to a URI. See
/// [PromptUserForConsent] for more information.
///
/// {@macro googleapis_auth_client_for_creds}
///
/// {@macro googleapis_auth_hostedDomain_param}
///
/// {@macro googleapis_auth_user_consent_return}
///
/// {@template googleapis_auth_listen_port}
/// The `localhost` port to use when listening for the redirect from a user
/// browser interaction. Defaults to `0` - which means the port is dynamic.
///
/// Generally you want to specify an explicit port so you can configure it
/// on the Google Cloud console.
/// {@endtemplate}
Future<AccessCredentials> obtainAccessCredentialsViaUserConsent(
AuthEndpoints authEndpoints,
ClientId clientId,
List<String> scopes,
Client client,
PromptUserForConsent userPrompt, {
String? hostedDomain,
int listenPort = 0,
}) =>
AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
client,
userPrompt,
hostedDomain: hostedDomain,
listenPort: listenPort,
).run();
/// Obtain oauth2 [AccessCredentials] using the oauth2 authentication code flow.
///
/// {@macro googleapis_auth_clientId_param}
///
/// [userPrompt] will be used for directing the user/user-agent to a URI. See
/// [PromptUserForConsentManual] for more information.
///
/// {@macro googleapis_auth_client_for_creds}
///
/// {@macro googleapis_auth_hostedDomain_param}
///
/// {@macro googleapis_auth_user_consent_return}
Future<AccessCredentials> obtainAccessCredentialsViaUserConsentManual(
AuthEndpoints authEndpoints,
ClientId clientId,
List<String> scopes,
Client client,
PromptUserForConsentManual userPrompt, {
String? hostedDomain,
}) =>
AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
client,
userPrompt,
hostedDomain: hostedDomain,
).run();
-34
View File
@@ -1,34 +0,0 @@
// Copyright (c) 2021, 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: comment_references
/// Contains common libraries used across the package.
///
/// In most cases, you'll want to import either
/// [auth_io] or [auth_browser] depending on your platform.
/// {@canonicalFor access_credentials.AccessCredentials}
/// {@canonicalFor access_token.AccessToken}
/// {@canonicalFor auth_client.AuthClient}
/// {@canonicalFor auth_client.AutoRefreshingAuthClient}
/// {@canonicalFor auth_functions.authenticatedClient}
/// {@canonicalFor auth_functions.autoRefreshingClient}
/// {@canonicalFor auth_functions.clientViaApiKey}
/// {@canonicalFor auth_functions.refreshCredentials}
/// {@canonicalFor client_id.ClientId}
/// {@canonicalFor exceptions.AccessDeniedException}
/// {@canonicalFor exceptions.ServerRequestFailedException}
/// {@canonicalFor exceptions.RefreshFailedException}
/// {@canonicalFor exceptions.UserConsentException}
/// {@canonicalFor response_type.ResponseType}
/// {@canonicalFor service_account_credentials.ServiceAccountCredentials}
library googleapis_auth;
export 'src/auth_client.dart';
export 'src/auth_functions.dart';
export 'src/auth_endpoints.dart';
export 'src/client_id.dart';
export 'src/exceptions.dart';
export 'src/response_type.dart';
export 'src/service_account_credentials.dart';
@@ -1,42 +0,0 @@
// Copyright (c) 2021, 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 'access_token.dart';
/// 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,
});
factory AccessCredentials.fromJson(Map<String, dynamic> json) =>
AccessCredentials(
AccessToken.fromJson(json['accessToken'] as Map<String, dynamic>),
json['refreshToken'] as String?,
(json['scopes'] as List<dynamic>).map((e) => e as String).toList(),
idToken: json['idToken'] as String?,
);
Map<String, dynamic> toJson() => <String, dynamic>{
'accessToken': accessToken,
if (refreshToken != null) 'refreshToken': refreshToken,
'idToken': idToken,
'scopes': scopes,
};
}
-43
View File
@@ -1,43 +0,0 @@
// Copyright (c) 2021, 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.
/// 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.value(
expiry,
'expiry',
'The expiry date must be a Utc DateTime.',
);
}
}
factory AccessToken.fromJson(Map<String, dynamic> json) => AccessToken(
json['type'] as String,
json['data'] as String,
DateTime.parse(json['expiry'] as String),
);
bool get hasExpired => DateTime.now().toUtc().isAfter(expiry);
@override
String toString() => 'AccessToken(type=$type, data=$data, expiry=$expiry)';
Map<String, dynamic> toJson() => <String, dynamic>{
'type': type,
'data': data,
'expiry': expiry.toIso8601String(),
};
}
-62
View File
@@ -1,62 +0,0 @@
// Copyright (c) 2021, 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:async';
import 'dart:convert';
import 'dart:io';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart';
import 'auth_http_utils.dart';
Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
File file,
AuthEndpoints authEndpoints,
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'] as String,
credentials['client_secret'] as String?,
);
return AutoRefreshingClient(
baseClient,
authEndpoints,
clientId,
await refreshCredentials(
authEndpoints,
clientId,
AccessCredentials(
// Hack: Create empty credentials that have expired.
AccessToken('Bearer', '', DateTime(0).toUtc()),
credentials['refresh_token'] as String?,
scopes,
),
baseClient,
),
quotaProject: credentials['quota_project_id'] as String?,
);
}
return await clientViaServiceAccount(
ServiceAccountCredentials.fromJson(credentials),
scopes,
baseClient: baseClient,
);
}
-21
View File
@@ -1,21 +0,0 @@
// Copyright (c) 2021, 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 'package:http/http.dart';
import 'access_credentials.dart';
/// A authenticated HTTP client.
abstract class AuthClient implements Client {
/// The credentials currently used for making HTTP requests.
AccessCredentials get credentials;
}
/// A auto-refreshing, 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;
}
-14
View File
@@ -1,14 +0,0 @@
import 'known_uris.dart';
abstract class AuthEndpoints {
Uri get authorizationEndpoint;
Uri get tokenEndpoint;
}
class GoogleAuthEndpoints extends AuthEndpoints {
@override
Uri get authorizationEndpoint => googleOauth2AuthorizationEndpoint;
@override
Uri get tokenEndpoint => googleOauth2TokenEndpoint;
}
-136
View File
@@ -1,136 +0,0 @@
// 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:async';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart';
import 'auth_http_utils.dart';
import 'http_client_base.dart';
import 'utils.dart';
/// Obtains a [Client] which uses the given [apiKey] for making HTTP
/// requests.
///
/// {@macro googleapis_auth_baseClient_param}
///
/// 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.
///
/// {@template googleapis_auth_close_the_client}
/// The user is responsible for closing the returned HTTP [Client].
/// {@endtemplate}
/// {@template googleapis_auth_not_close_the_baseClient}
/// Closing the returned [Client] will not close [baseClient].
/// {@endtemplate}
Client clientViaApiKey(
String apiKey, {
Client? baseClient,
}) {
if (baseClient == null) {
baseClient = Client();
} else {
baseClient = nonClosingClient(baseClient);
}
return ApiKeyClient(baseClient, apiKey);
}
/// Obtain a [Client] which automatically authenticates requests using
/// [credentials].
///
/// Note that the returned [AuthClient] will not auto-refresh the given
/// [credentials].
///
/// {@macro googleapis_auth_close_the_client}
///
/// If [closeUnderlyingClient] is `true`, [AuthClient.close] will also close
/// [baseClient].
///
/// {@macro googleapis_auth_not_close_the_baseClient}
AuthClient authenticatedClient(
Client baseClient,
AccessCredentials credentials, {
bool closeUnderlyingClient = false,
}) {
if (credentials.accessToken.type != 'Bearer') {
throw ArgumentError('Only Bearer access tokens are accepted.');
}
return AuthenticatedClient(
baseClient,
credentials,
closeUnderlyingClient: closeUnderlyingClient,
);
}
/// Creates an [AutoRefreshingAuthClient] which automatically refreshes
/// [credentials] before they expire.
///
/// {@template googleapis_auth_clientId_param}
/// The [clientId] that you obtain from the API Console
/// [Credentials page](https://console.developers.google.com/apis/credentials),
/// as described in
/// [Obtain OAuth 2.0 credentials](https://developers.google.com/identity/protocols/oauth2/openid-connect#getcredentials).
/// {@endtemplate}
///
/// Uses [baseClient] to make authenticated HTTP requests and to refresh
/// [credentials].
///
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
AutoRefreshingAuthClient autoRefreshingClient(
AuthEndpoints authEndpoints,
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, authEndpoints, clientId, credentials);
}
/// Obtains refreshed [AccessCredentials] for [clientId] and [credentials].
///
/// {@macro googleapis_auth_clientId_param}
///
/// {@macro googleapis_auth_client_for_creds}
Future<AccessCredentials> refreshCredentials(
AuthEndpoints authEndpoints,
ClientId clientId,
AccessCredentials credentials,
Client client,
) async {
final refreshToken = credentials.refreshToken;
if (refreshToken == null) {
throw ArgumentError('clientId.refreshToken cannot be null.');
}
// https://developers.google.com/identity/protocols/oauth2/native-app#offline
final jsonMap = await client.oauthTokenRequest(
{
'client_id': clientId.identifier,
// Not all providers require a client secret,
// e.g. https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow#refresh-the-access-token
if (clientId.secret != null) 'client_secret': clientId.secret!,
'refresh_token': refreshToken,
'grant_type': 'refresh_token',
},
authEndpoints: authEndpoints,
);
final accessToken = parseAccessToken(jsonMap);
final idToken = jsonMap['id_token'] as String?;
return AccessCredentials(
accessToken,
credentials.refreshToken,
credentials.scopes,
idToken: idToken,
);
}
-156
View File
@@ -1,156 +0,0 @@
// 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:async';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.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(
super.client,
this.credentials, {
this.quotaProject,
super.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(super.client, String apiKey)
: _encodedApiKey = Uri.encodeQueryComponent(apiKey),
super(closeUnderlyingClient: true);
@override
Future<StreamedResponse> send(BaseRequest request) async {
var url = request.url;
if (url.queryParameters.containsKey('key')) {
throw ArgumentError(
'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())
..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;
final AuthEndpoints authEndpoints;
AutoRefreshingClient(
super.client,
this.authEndpoints,
this.clientId,
this.credentials, {
super.closeUnderlyingClient,
this.quotaProject,
}) : assert(credentials.accessToken.type == 'Bearer'),
assert(credentials.refreshToken != null) {
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(
authEndpoints,
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(
super.client, {
super.closeUnderlyingClient,
});
@override
Stream<AccessCredentials> get credentialUpdates =>
_credentialStreamController.stream;
void notifyAboutNewCredentials(AccessCredentials credentials) {
_credentialStreamController.add(credentials);
}
@override
void close() {
_credentialStreamController.close();
super.close();
}
}
@@ -1,22 +0,0 @@
/// Exception thrown when authentication fails.
class AuthenticationException implements Exception {
final String error;
/// Human-readable ASCII text providing additional information, used to assist
/// the client developer in understanding the error that occurred.
final String? errorDescription;
/// A URI identifying a human-readable web page with information about the
/// error, used to provide the client developer with additional information
/// about the error.
final String? errorUri;
AuthenticationException(
this.error, {
this.errorDescription,
this.errorUri,
});
@override
String toString() => 'AuthenticationException: $error';
}
-112
View File
@@ -1,112 +0,0 @@
import 'dart:async';
import 'dart:html' as html;
import 'dart:js' as js;
import 'authentication_exception.dart';
Future<void> initializeScript(
String scriptUrl, {
String? onloadParam,
}) async {
final instance = _ScriptLoader._instances.putIfAbsent(
scriptUrl,
() => _ScriptLoader._(scriptUrl, onloadParam: onloadParam),
);
await instance._initialize();
}
/// 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() _createScriptTag = (() {
final nonce = _getNonce();
if (nonce == null) return html.ScriptElement.new;
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;
}
// 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}\$');
const callbackTimeout = Duration(seconds: 20);
class _ScriptLoader {
_ScriptLoader._(
this.url, {
this.onloadParam,
});
static final _instances = <String, _ScriptLoader>{};
final String url;
final String? onloadParam;
Future<void>? _pendingInitialization;
Future<void> _initialize() {
if (_pendingInitialization != null) {
return _pendingInitialization!;
}
final completer = Completer();
final timeout = Timer(callbackTimeout, () {
_pendingInitialization = null;
completer.completeError(
AuthenticationException(
'Timed out while waiting for library to load: $url',
),
);
});
void loadComplete() {
timeout.cancel();
completer.complete();
}
final loadFunctionName = '_dartScriptLoad_${url.hashCode}';
js.context[loadFunctionName] = loadComplete;
final fullUrl =
onloadParam == null ? url : '$url?${onloadParam!}=$loadFunctionName';
final script = _createScriptTag()
..async = true
..defer = true
..src = fullUrl;
if (onloadParam == null) {
script.onLoad.first.then((event) {
loadComplete();
});
}
script.onError.first.then((errorEvent) {
timeout.cancel();
_pendingInitialization = null;
if (!completer.isCompleted) {
// script loading errors can still happen after timeouts
completer.completeError(
AuthenticationException('Failed to load library: $url'));
}
});
html.document.body!.append(script);
_pendingInitialization = completer.future;
return completer.future;
}
}
-29
View File
@@ -1,29 +0,0 @@
// Copyright (c) 2021, 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.
/// Represents the client application's credentials.
class ClientId {
/// The client ID that you obtain from the API Console
/// [Credentials page](https://console.developers.google.com/apis/credentials),
/// as described in
/// [Obtain OAuth 2.0 credentials](https://developers.google.com/identity/protocols/oauth2/openid-connect#getcredentials).
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;
factory ClientId.fromJson(Map<String, dynamic> json) => ClientId(
json['identifier'] as String,
json['secret'] as String?,
);
Map<String, dynamic> toJson() => {
'identifier': identifier,
if (secret != null) 'secret': secret,
};
}
-141
View File
@@ -1,141 +0,0 @@
// 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:typed_data';
import 'rsa.dart';
// ignore: avoid_classes_with_only_static_members
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 {}
-104
View File
@@ -1,104 +0,0 @@
// 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: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',
);
}
}
-120
View File
@@ -1,120 +0,0 @@
// 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.
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,
);
}
// ignore: avoid_classes_with_only_static_members
/// 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;
}
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);
@@ -1,80 +0,0 @@
// 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: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;
}
}
-60
View File
@@ -1,60 +0,0 @@
// Copyright (c) 2021, 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.
/// No longer used. Replaced by [ServerRequestFailedException].
@Deprecated('No longer used. Replaced by ServerRequestFailedException.')
typedef RefreshFailedException = ServerRequestFailedException;
/// 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 their consent.
class UserConsentException implements Exception {
final String message;
final String? details;
UserConsentException(this.message, {this.details});
@override
String toString() => [message, if (details != null) details].join(' ');
}
/// Thrown when a request to or the response from an authentication service is
/// invalid.
///
/// This could indicate invalid credentials.
class ServerRequestFailedException implements Exception {
/// Describes the failure.
final String message;
/// The HTTP status code of the response, if known.
///
/// If `null`, the status code was likely `200` and there was another issue
/// with the response.
final int? statusCode;
/// Data representing the content of the response, if any.
///
/// This may be a [String] representing the raw content of the response or
/// the a parsed JSON literal of the content.
final Object? responseContent;
ServerRequestFailedException(
this.message, {
this.statusCode,
required this.responseContent,
});
@override
String toString() =>
[message, if (statusCode != null) 'Status code: $statusCode'].join(' ');
}
@@ -1,102 +0,0 @@
// 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:async';
import 'package:http/http.dart';
/// Base class for delegating HTTP clients.
///
/// If [closeUnderlyingClient] is `true`, [close] will also close [baseClient].
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(super.baseClient, {int initialRefCount = 1})
: _refCount = initialRefCount,
super(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(super.method, super.url, [Stream<List<int>>? stream])
: _stream = stream ?? const Stream.empty();
@override
ByteStream finalize() {
super.finalize();
return ByteStream(_stream);
}
}
-12
View File
@@ -1,12 +0,0 @@
// Copyright (c) 2021, 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.
/// token_endpoint
/// via https://accounts.google.com/.well-known/openid-configuration
final googleOauth2TokenEndpoint = Uri.https('oauth2.googleapis.com', 'token');
/// authorization_endpoint
/// via https://accounts.google.com/.well-known/openid-configuration
final googleOauth2AuthorizationEndpoint =
Uri.https('accounts.google.com', 'o/oauth2/v2/auth');
@@ -1,46 +0,0 @@
// Copyright (c) 2021, 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 'package:http/http.dart';
import 'access_credentials.dart';
import 'auth_client.dart';
import 'oauth2_flows/base_flow.dart';
import 'oauth2_flows/metadata_server.dart';
/// 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`.
///
/// {@template googleapis_auth_client_for_creds}
/// [client] will be used for making the HTTP requests needed to create the
/// returned [AccessCredentials].
/// {@endtemplate}
///
/// 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 client,
) =>
MetadataServerAuthorizationFlow(client).run();
/// Obtains oauth2 credentials and returns an authenticated HTTP client.
///
/// See [obtainAccessCredentialsViaMetadataServer] for specifics about the
/// arguments used for obtaining access credentials.
///
/// {@macro googleapis_auth_returned_auto_refresh_client}
///
/// {@macro googleapis_auth_baseClient_param}
///
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
Future<AutoRefreshingAuthClient> clientViaMetadataServer({
Client? baseClient,
}) async =>
await clientFromFlow(
MetadataServerAuthorizationFlow.new,
baseClient: baseClient,
);
@@ -1,156 +0,0 @@
// 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:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
import '../utils.dart';
Uri createAuthenticationUri({
required AuthEndpoints authEndpoints,
required String redirectUri,
required String clientId,
required Iterable<String> scopes,
required String codeVerifier,
String? hostedDomain,
String? state,
bool offline = false,
}) {
final queryValues = {
'client_id': clientId,
'response_type': 'code',
'redirect_uri': redirectUri,
'scope': scopes.join(' '),
'code_challenge': _codeVerifierShaEncode(codeVerifier),
'code_challenge_method': 'S256',
if (offline) 'access_type': 'offline',
if (hostedDomain != null) 'hd': hostedDomain,
if (state != null) 'state': state,
};
return authEndpoints.authorizationEndpoint.replace(
queryParameters: queryValues,
);
}
/// https://developers.google.com/identity/protocols/oauth2/native-app#create-code-challenge
/// https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
String createCodeVerifier() {
final rnd = Random.secure();
return List.generate(128, (index) => _safe[rnd.nextInt(_safe.length)]).join();
}
/// See https://developers.google.com/identity/protocols/oauth2/openid-connect#createxsrftoken
String randomState() {
final rnd = Random.secure();
final list = Uint32List(6);
for (var i = 0; i < list.length; i++) {
list[i] = rnd.nextInt(1 << 32);
}
final value = base64UrlEncode(Uint8List.view(list.buffer));
return _stripBase64Equals(value);
}
// https://datatracker.ietf.org/doc/html/rfc3986#section-2.3
const _safe = '0123456789-._~'
'abcdefghijklmnopqrstuvwxyz'
'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
String _codeVerifierShaEncode(String codeVerifier) {
final asciiBytes = ascii.encode(codeVerifier);
final sha26Bytes = sha256.convert(asciiBytes).bytes;
final output = base64UrlEncode(sha26Bytes);
return _stripBase64Equals(output);
}
String _stripBase64Equals(String value) {
while (value.endsWith('=')) {
value = value.substring(0, value.length - 1);
}
return value;
}
/// 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].
///
/// {@macro googleapis_auth_client_for_creds}
///
/// {@macro googleapis_auth_clientId_param}
///
/// 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.
Future<AccessCredentials> obtainAccessCredentialsViaCodeExchange(
AuthEndpoints authEndpoints,
http.Client client,
ClientId clientId,
String code, {
String redirectUrl = 'postmessage',
String? codeVerifier,
}) async {
final jsonMap = await client.oauthTokenRequest(
{
'client_id': clientId.identifier,
'client_secret': clientId.secret ?? '',
'code': code,
if (codeVerifier != null) 'code_verifier': codeVerifier,
'grant_type': 'authorization_code',
'redirect_uri': redirectUrl,
},
authEndpoints: authEndpoints,
);
final accessToken = parseAccessToken(jsonMap);
final idToken = jsonMap['id_token'] as String?;
final refreshToken = jsonMap['refresh_token'] as String?;
final scope = jsonMap['scope'];
if (scope is! String) {
throw ServerRequestFailedException(
'The response did not include a `scope` value of type `String`.',
responseContent: json,
);
}
final scopes = scope.split(' ').toList();
return AccessCredentials(
accessToken,
refreshToken,
scopes,
idToken: idToken,
);
}
List<String> parseScopes(Map<String, dynamic> json) {
final scope = json['scope'];
if (scope is! String) {
throw ServerRequestFailedException(
'The response did not include a `scope` value of type `String`.',
responseContent: json,
);
}
return scope.split(' ').toList();
}
@@ -1,57 +0,0 @@
// Copyright (c) 2021, 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 'package:http/http.dart' as http;
import '../access_credentials.dart';
import '../auth_endpoints.dart';
import '../client_id.dart';
import 'auth_code.dart';
import 'base_flow.dart';
abstract class AuthorizationCodeGrantAbstractFlow implements BaseFlow {
final AuthEndpoints authEndpoints;
final ClientId clientId;
final String? hostedDomain;
final List<String> scopes;
final http.Client _client;
AuthorizationCodeGrantAbstractFlow(
this.authEndpoints,
this.clientId,
this.scopes,
this._client, {
this.hostedDomain,
});
Future<AccessCredentials> obtainAccessCredentialsUsingCodeImpl(
String code,
String redirectUri, {
required AuthEndpoints authEndpoints,
required String codeVerifier,
}) =>
obtainAccessCredentialsViaCodeExchange(
authEndpoints,
_client,
clientId,
code,
redirectUrl: redirectUri,
codeVerifier: codeVerifier,
);
Uri authenticationUri(
String redirectUri, {
String? state,
required String codeVerifier,
}) =>
createAuthenticationUri(
authEndpoints: authEndpoints,
redirectUri: redirectUri,
clientId: clientId.identifier,
scopes: scopes,
codeVerifier: codeVerifier,
hostedDomain: hostedDomain,
state: state,
);
}
@@ -1,57 +0,0 @@
// Copyright (c) 2021, 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:async';
import '../access_credentials.dart';
import '../typedefs.dart';
import 'auth_code.dart';
import 'authorization_code_grant_abstract_flow.dart';
/// 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(
super.authEndpoints,
super.clientId,
super.scopes,
super.client,
this.userPrompt, {
super.hostedDomain,
});
@override
Future<AccessCredentials> run() async {
final codeVerifier = createCodeVerifier();
// Prompt user and wait until they goes to URL and copy&pastes the auth code
// in.
final code = await userPrompt(
authenticationUri(
_redirectionUri,
codeVerifier: codeVerifier,
).toString(),
);
// Use code to obtain credentials
return obtainAccessCredentialsUsingCodeImpl(
code,
_redirectionUri,
authEndpoints: authEndpoints,
codeVerifier: codeVerifier,
);
}
}
const _redirectionUri = 'urn:ietf:wg:oauth:2.0:oob';
@@ -1,128 +0,0 @@
// Copyright (c) 2021, 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:async';
import 'dart:io';
import '../access_credentials.dart';
import '../exceptions.dart';
import '../typedefs.dart';
import 'auth_code.dart';
import 'authorization_code_grant_abstract_flow.dart';
/// 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;
final int listenPort;
AuthorizationCodeGrantServerFlow(
super.authEndpoints,
super.clientId,
super.scopes,
super.client,
this.userPrompt, {
super.hostedDomain,
this.listenPort = 0,
});
@override
Future<AccessCredentials> run() async {
final server = await HttpServer.bind('localhost', listenPort);
try {
final port = server.port;
final redirectionUri = 'http://localhost:$port';
final state = randomState();
final codeVerifier = createCodeVerifier();
// Prompt user and wait until they goes to URL and the google
// authorization server calls back to our locally running HTTP server.
userPrompt(
authenticationUri(
redirectionUri,
state: state,
codeVerifier: codeVerifier,
).toString(),
);
final request = await server.first;
final uri = request.uri;
try {
if (request.method != 'GET') {
throw Exception(
'Invalid response from server '
'(expected GET request callback, got: ${request.method}).',
);
}
final returnedState = uri.queryParameters['state'];
if (state != returnedState) {
throw Exception(
'Invalid response from server (state did not match).',
);
}
final error = uri.queryParameters['error'];
if (error != null) {
throw UserConsentException(
'Error occurred while obtaining access credentials: $error',
);
}
final code = uri.queryParameters['code'];
if (code == null || code.isEmpty) {
throw Exception(
'Invalid response from server (no auth code transmitted).',
);
}
final credentials = await obtainAccessCredentialsUsingCodeImpl(
code,
redirectionUri,
authEndpoints: authEndpoints,
codeVerifier: codeVerifier,
);
// 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();
}
}
}
@@ -1,55 +0,0 @@
import 'package:http/http.dart';
import '../access_credentials.dart';
import '../auth_client.dart';
import '../auth_functions.dart';
import '../auth_http_utils.dart';
import '../http_client_base.dart';
/// Base class for "Flows" that provide [AccessCredentials].
abstract class BaseFlow {
Future<AccessCredentials> run();
}
Future<AutoRefreshingAuthClient> clientFromFlow(
BaseFlow Function(Client client) flowFactory, {
Client? baseClient,
}) async {
if (baseClient == null) {
baseClient = Client();
} else {
baseClient = nonClosingClient(baseClient);
}
final flow = flowFactory(baseClient);
try {
final credentials = await flow.run();
return _FlowClient(baseClient, credentials, flow);
} catch (e) {
baseClient.close();
rethrow;
}
}
// Will close the underlying `http.Client`.
class _FlowClient extends AutoRefreshDelegatingClient {
final BaseFlow _flow;
@override
AccessCredentials credentials;
Client _authClient;
_FlowClient(super.client, this.credentials, this._flow)
: _authClient = authenticatedClient(client, credentials);
@override
Future<StreamedResponse> send(BaseRequest request) async {
if (credentials.accessToken.hasExpired) {
final newCredentials = await _flow.run();
notifyAboutNewCredentials(newCredentials);
credentials = newCredentials;
_authClient = authenticatedClient(baseClient, credentials);
}
return _authClient.send(request);
}
}
@@ -1,206 +0,0 @@
// 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:async';
import 'dart:html' as html;
import 'dart:js' as js;
import '../access_credentials.dart';
import '../access_token.dart';
import '../authentication_exception.dart';
import '../browser_utils.dart';
import '../exceptions.dart';
import '../response_type.dart';
// This will be overridden by tests.
String gapiUrl = 'https://apis.google.com/js/client.js';
/// 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 {
final String _clientId;
final List<String> _scopes;
final bool _enableDebugLogs;
ImplicitFlow(this._clientId, this._scopes, this._enableDebugLogs);
/// 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() async {
await initializeScript(gapiUrl, onloadParam: 'onload');
if (_enableDebugLogs) _gapiAuth2.callMethod('enableDebugLogs', [true]);
}
Future<LoginResult> loginHybrid({
String? prompt,
String? loginHint,
String? hostedDomain,
}) =>
_login(
prompt: prompt,
responseTypes: [ResponseType.code, ResponseType.token],
loginHint: loginHint,
hostedDomain: hostedDomain,
);
Future<AccessCredentials> login({
String? prompt,
String? loginHint,
List<ResponseType>? responseTypes,
String? hostedDomain,
}) async =>
(await _login(
prompt: prompt,
loginHint: loginHint,
responseTypes: responseTypes,
hostedDomain: hostedDomain,
))
.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({
required String? prompt,
required String? hostedDomain,
required String? loginHint,
required List<ResponseType>? responseTypes,
}) {
final completer = Completer<LoginResult>();
// https://developers.google.com/identity/sign-in/web/reference#gapiauth2authorizeconfig
final json = {
'client_id': _clientId,
'scope': _scopes.join(' '),
'response_type': responseTypes == null || responseTypes.isEmpty
? 'token'
: responseTypes.map(_responseTypeToString).join(' '),
if (prompt != null) 'prompt': prompt,
// cookie_policy missing
if (hostedDomain != null) 'hosted_domain': hostedDomain,
if (loginHint != null) 'login_hint': loginHint,
// include_granted_scopes - missing
'plugin_name': 'dart-googleapis_auth',
};
_gapiAuth2.callMethod('authorize', [
js.JsObject.jsify(json),
(js.JsObject jsTokenObject) {
try {
final result = _processToken(jsTokenObject, responseTypes);
completer.complete(result);
} catch (e, stack) {
html.window.console.error(jsTokenObject);
completer.completeError(e, stack);
}
}
]);
return completer.future;
}
LoginResult _processToken(
js.JsObject jsTokenObject,
List<ResponseType>? responseTypes,
) {
final error = jsTokenObject['error'];
if (error != null) {
final details = jsTokenObject['details'] as String?;
throw UserConsentException(
'Failed to get user consent: $error.',
details: details,
);
}
final tokenType = jsTokenObject['token_type'];
final token = jsTokenObject['access_token'] as String?;
if (token == null || tokenType != 'Bearer') {
throw AuthenticationException(
'Failed to obtain user consent. Invalid server response.',
);
}
final idToken = jsTokenObject['id_token'] as String?;
if (responseTypes?.contains(ResponseType.idToken) == true &&
idToken?.isNotEmpty != true) {
throw AuthenticationException('Expected to get id_token, but did not.');
}
final scopeString = jsTokenObject['scope'] as String;
final scopes = scopeString.split(' ');
final expiresAt = jsTokenObject['expires_at'] as int;
final expiresAtDate =
DateTime.fromMillisecondsSinceEpoch(expiresAt).toUtc();
final accessToken = AccessToken('Bearer', token, expiresAtDate);
final credentials = AccessCredentials(
accessToken,
null,
scopes,
idToken: idToken,
);
String? code;
if (responseTypes?.contains(ResponseType.code) == true) {
code = jsTokenObject['code'] as String?;
if (code == null) {
throw AuthenticationException(
'Expected to get auth code from server in hybrid flow, but did not.',
);
}
}
return LoginResult(credentials, code: code);
}
}
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) {
switch (responseType) {
case ResponseType.code:
return 'code';
case ResponseType.idToken:
return 'id_token';
case ResponseType.permission:
return 'permission';
case ResponseType.token:
return 'token';
}
}
js.JsObject get _gapiAuth2 =>
(js.context['gapi'] as js.JsObject)['auth2'] as js.JsObject;
@@ -1,77 +0,0 @@
// 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:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../auth_io.dart';
import '../crypto/rsa.dart';
import '../crypto/rsa_sign.dart';
import '../known_uris.dart';
import '../utils.dart';
import 'base_flow.dart';
/// Currently only supports the Google auth provider.
class JwtFlow extends BaseFlow {
// All details are described at:
// https://developers.google.com/accounts/docs/OAuth2ServiceAccount
// JSON Web Signature (JWS) requires signing a string with a private key.
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);
@override
Future<AccessCredentials> run() async {
final timestamp = DateTime.now().toUtc().millisecondsSinceEpoch ~/ 1000 -
maxExpectedTimeDiffInSeconds;
final jwtHeader = {'alg': 'RS256', 'typ': 'JWT'};
final jwtHeaderBase64 = _base64url(ascii.encode(jsonEncode(jwtHeader)));
final jwtClaimSet = {
'iss': _clientEmail,
'scope': _scopes.join(' '),
'aud': googleOauth2TokenEndpoint.toString(),
'exp': timestamp + 3600,
'iat': timestamp,
if (_user != null) 'sub': _user!,
};
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)}';
// https://developers.google.com/identity/protocols/oauth2/service-account#authorizingrequests
final response = await _client.oauthTokenRequest(
{
'grant_type': _uri,
'assertion': jwt,
},
authEndpoints: GoogleAuthEndpoints(),
);
final accessToken = parseAccessToken(response);
return AccessCredentials(accessToken, null, _scopes);
}
}
const _uri = 'urn:ietf:params:oauth:grant-type:jwt-bearer';
String _base64url(List<int> bytes) =>
base64Url.encode(bytes).replaceAll('=', '');
@@ -1,92 +0,0 @@
// 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:async';
import 'dart:io';
import 'package:http/http.dart' as http;
import '../access_credentials.dart';
import '../utils.dart';
import 'base_flow.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 extends BaseFlow {
static const _headers = {'Metadata-Flavor': 'Google'};
static const _serviceAccountUrlInfix =
'computeMetadata/v1/instance/service-accounts';
// https://cloud.google.com/compute/docs/storing-retrieving-metadata#querying
static const _defaultMetadataHost = 'metadata.google.internal';
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,
);
@override
Future<AccessCredentials> run() async {
final results = await Future.wait(
[
_client.requestJson(
http.Request('GET', _tokenUrl)..headers.addAll(_headers),
'Failed to obtain access credentials.',
),
_getScopes()
],
);
final json = results.first as Map<String, dynamic>;
final accessToken = parseAccessToken(json);
final scopes = (results.last as String)
.replaceAll('\n', ' ')
.split(' ')
.where((part) => part.isNotEmpty)
.toList();
return AccessCredentials(
accessToken,
null,
scopes,
);
}
Future<String> _getScopes() async {
final response = await _client.get(_scopesUrl, headers: _headers);
return response.body;
}
}
@@ -1,178 +0,0 @@
import 'dart:async';
import 'dart:html';
import 'dart:js';
import '../access_credentials.dart';
import '../access_token.dart';
import '../authentication_exception.dart';
import '../browser_utils.dart';
import '../utils.dart';
import 'token_model_interop.dart' as interop;
JsObject get _googleAccountsId =>
((context['google'] as JsObject)['accounts'] as JsObject)['id'] as JsObject;
/// Obtains [AccessCredentials] using the
/// [Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/overview)
/// token model.
///
/// The returned [AccessCredentials] will *always* have a `null` value for
/// [AccessCredentials.refreshToken] and
/// [AccessCredentials.idToken].
///
/// See
/// [Choose a user authorization model](https://developers.google.com/identity/oauth2/web/guides/choose-authorization-model)
/// to understand the tradeoffs between using this function and
/// [requestAuthorizationCode].
///
/// See https://developers.google.com/identity/oauth2/web/guides/use-token-model
/// and https://developers.google.com/identity/oauth2/web/reference/js-reference
/// for more details.
Future<AccessCredentials> requestAccessCredentials({
required String clientId,
required Iterable<String> scopes,
String prompt = 'select_account',
@Deprecated('Undocumented feature. Do not include in production code.')
String? logLevel,
}) async {
await initializeScript('https://accounts.google.com/gsi/client');
if (logLevel != null) _googleAccountsId.callMethod('setLogLevel', [logLevel]);
final completer = Completer<AccessCredentials>();
void callback(interop.TokenResponse response) {
if (response.error != null) {
window.console.log(response);
completer.completeError(
AuthenticationException(
response.error!,
errorDescription: response.error_description,
errorUri: response.error_uri,
),
);
return;
}
final token = AccessToken(
response.token_type,
response.access_token,
expiryDate(response.expires_in),
);
final creds = AccessCredentials(token, null, response.scope.split(' '));
completer.complete(creds);
}
final config = interop.TokenClientConfig(
callback: allowInterop(callback),
client_id: clientId,
scope: scopes.toSet().join(' '),
prompt: prompt,
);
final client = interop.initTokenClient(config);
client.requestAccessToken();
return completer.future;
}
/// Obtains [CodeResponse] using the
/// [Google Identity Services](https://developers.google.com/identity/oauth2/web/guides/overview)
/// code model.
///
/// See
/// [Choose a user authorization model](https://developers.google.com/identity/oauth2/web/guides/choose-authorization-model)
/// to understand the tradeoffs between using this function and
/// [requestAccessCredentials].
///
/// See https://developers.google.com/identity/oauth2/web/guides/use-code-model
/// and https://developers.google.com/identity/oauth2/web/reference/js-reference
/// for more details.
Future<CodeResponse> requestAuthorizationCode({
required String clientId,
required Iterable<String> scopes,
String? state,
String? hint,
String? hostedDomain,
@Deprecated('Undocumented feature. Do not include in production code.')
String? logLevel,
}) async {
await initializeScript('https://accounts.google.com/gsi/client');
if (logLevel != null) _googleAccountsId.callMethod('setLogLevel', [logLevel]);
final completer = Completer<CodeResponse>();
void callback(interop.CodeResponse response) {
if (response.error != null) {
window.console.log(response);
completer.completeError(
AuthenticationException(
response.error!,
errorDescription: response.error_description,
errorUri: response.error_uri,
),
);
return;
}
completer.complete(CodeResponse._(
code: response.code,
scopes: response.scope.split(' '),
state: response.state,
));
}
final config = interop.CodeClientConfig(
callback: allowInterop(callback),
client_id: clientId,
scope: scopes.toSet().join(' '),
state: state,
hint: hint,
hosted_domain: hostedDomain,
);
final client = interop.initCodeClient(config);
client.requestCode();
return completer.future;
}
/// Revokes all of the scopes that the user granted to the app.
///
/// A valid [accessTokenValue] is required to revoke the permission.
Future<void> revokeConsent(String accessTokenValue) {
final completer = Completer<void>();
void done(Object? arg) {
window.console.log(arg);
completer.complete();
}
interop.revoke(accessTokenValue, allowInterop(done));
return completer.future;
}
/// Result from a successful call to [requestAuthorizationCode].
///
/// See https://developers.google.com/identity/oauth2/web/reference/js-reference#CodeResponse
/// for more details.
class CodeResponse {
CodeResponse._({required this.code, required this.scopes, this.state});
/// The authorization code of a successful token response.
final String code;
/// The list of scopes that are approved by the user.
final List<String> scopes;
/// The string value that your application uses to maintain state between your
/// authorization request and the response.
final String? state;
@override
String toString() => 'CodeResponse: $code';
}
@@ -1,115 +0,0 @@
// ignore_for_file: non_constant_identifier_names
@JS('google.accounts.oauth2')
library token_model_interop;
import 'package:js/js.dart';
@JS()
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#google.accounts.oauth2.initTokenClient
external TokenClient initTokenClient(TokenClientConfig config);
@JS()
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#google.accounts.oauth2.revoke
external void revoke(String accessToken, [void Function(Object?) done]);
@JS()
@anonymous
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#TokenClientConfig
class TokenClientConfig {
external factory TokenClientConfig({
required String client_id,
required String scope,
required void Function(TokenResponse) callback,
String hint,
String hosted_domain,
String prompt,
});
// state: not recommended
// enable_serial_consent: skipping. only for old clients
}
@JS()
@anonymous
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#TokenResponse
class TokenResponse {
external String get access_token;
external int get expires_in;
external String get hd;
external String get prompt;
external String get token_type;
external String get scope;
/// A single ASCII error code.
external String? get error;
/// Human-readable ASCII text providing additional information, used to assist
/// the client developer in understanding the error that occurred.
external String? get error_description;
/// A URI identifying a human-readable web page with information about the
/// error, used to provide the client developer with additional information
/// about the error.
external String? get error_uri;
}
@JS()
@anonymous
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#google.accounts.oauth2.initTokenClient
class TokenClient {
external void requestAccessToken();
}
@JS()
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#google.accounts.oauth2.initCodeClient
external CodeClient initCodeClient(CodeClientConfig config);
@JS()
@anonymous
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#CodeClient
class CodeClient {
external void requestCode();
}
@JS()
@anonymous
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#CodeClientConfig
class CodeClientConfig {
external factory CodeClientConfig({
required String client_id,
required String scope,
String? redirect_uri,
required void Function(CodeResponse) callback,
String? state,
String? hint,
String? hosted_domain,
String? ux_mode,
bool select_account,
});
}
@JS()
@anonymous
// https://developers.google.com/identity/oauth2/web/reference/js-reference?hl=en#CodeResponse
class CodeResponse {
external String get code;
external String get scope;
external String? get state;
external String get authuser;
external String get hd;
external String get prompt;
/// A single ASCII error code.
external String? get error;
/// Human-readable ASCII text providing additional information, used to assist
/// the client developer in understanding the error that occurred.
external String? get error_description;
/// A URI identifying a human-readable web page with information about the
/// error, used to provide the client developer with additional information
/// about the error.
external String? get error_uri;
}
-23
View File
@@ -1,23 +0,0 @@
// Copyright (c) 2021, 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.
/// 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/oauth2/openid-connect#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,
}
@@ -1,57 +0,0 @@
// Copyright (c) 2021, 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 'package:http/http.dart';
import 'oauth2_flows/base_flow.dart';
import 'oauth2_flows/jwt.dart';
import 'service_account_credentials.dart';
/// 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`.
///
/// {@macro googleapis_auth_client_for_creds}
///
/// The [ServiceAccountCredentials] can be obtained in the Google Cloud Console.
Future<AccessCredentials> obtainAccessCredentialsViaServiceAccount(
ServiceAccountCredentials clientCredentials,
List<String> scopes,
Client client,
) =>
JwtFlow(
clientCredentials.email,
clientCredentials.privateRSAKey,
clientCredentials.impersonatedUser,
scopes,
client,
).run();
/// Obtains oauth2 credentials and returns an authenticated HTTP client.
///
/// See [obtainAccessCredentialsViaServiceAccount] for specifics about the
/// arguments used for obtaining access credentials.
///
/// {@macro googleapis_auth_returned_auto_refresh_client}
///
/// {@macro googleapis_auth_baseClient_param}
///
/// {@macro googleapis_auth_close_the_client}
/// {@macro googleapis_auth_not_close_the_baseClient}
Future<AutoRefreshingAuthClient> clientViaServiceAccount(
ServiceAccountCredentials clientCredentials,
List<String> scopes, {
Client? baseClient,
}) async =>
await clientFromFlow(
(c) => JwtFlow(
clientCredentials.email,
clientCredentials.privateRSAKey,
clientCredentials.impersonatedUser,
scopes,
c,
),
baseClient: baseClient,
);
@@ -1,95 +0,0 @@
// 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:convert';
import 'client_id.dart';
import 'crypto/pem.dart';
import 'crypto/rsa.dart';
export 'access_credentials.dart' show AccessCredentials;
export 'access_token.dart' show AccessToken;
export 'auth_client.dart';
export 'client_id.dart';
export 'exceptions.dart';
export 'response_type.dart';
/// 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(Object? 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'] as String?;
final privateKey = json['private_key'] as String?;
final email = json['client_email'] as String?;
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);
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);
}
-19
View File
@@ -1,19 +0,0 @@
// 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 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 their 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 their 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);
-171
View File
@@ -1,171 +0,0 @@
// 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:convert';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' show BaseRequest, Client, StreamedResponse;
import 'package:http_parser/http_parser.dart';
import 'http_client_base.dart';
/// Due to differences of clock speed, network latency, etc. we
/// will shorten expiry dates by 20 seconds.
const maxExpectedTimeDiffInSeconds = 20;
AccessToken parseAccessToken(Map<String, dynamic> jsonMap) {
final tokenType = jsonMap['token_type'];
final accessToken = jsonMap['access_token'];
final expiresIn = jsonMap['expires_in'];
if (accessToken is! String || expiresIn is! int || tokenType != 'Bearer') {
throw ServerRequestFailedException(
'Failed to exchange authorization code. Invalid server response.',
responseContent: jsonMap,
);
}
return AccessToken('Bearer', accessToken, expiryDate(expiresIn));
}
/// 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';
Future<Map<String, dynamic>> _readJsonMapFromResponse(
StreamedResponse response,
) async {
await _expectJsonResponse(response);
Object? jsonValue;
final bytes = await response.stream.toBytes();
late String string;
try {
string = utf8.decode(bytes);
} on FormatException catch (e) {
throw ServerRequestFailedException(
'The response was not valid UTF-8. '
'$e',
statusCode: response.statusCode,
responseContent: bytes,
);
}
try {
jsonValue = jsonDecode(string);
} on FormatException catch (e) {
throw ServerRequestFailedException(
'Could not decode the response as JSON. '
'$e',
statusCode: response.statusCode,
responseContent: string,
);
}
if (jsonValue is! Map<String, dynamic>) {
throw ServerRequestFailedException(
'The returned JSON response was not a Map.',
statusCode: response.statusCode,
responseContent: jsonValue,
);
}
return jsonValue;
}
extension ClientExtensions on Client {
Future<Map<String, dynamic>> requestJson(
BaseRequest request,
String errorHeader,
) async {
final response = await send(request);
final jsonMap = await _readJsonMapFromResponse(response);
if (response.statusCode != 200) {
final error = _errorStringFromJsonResponse(jsonMap);
final message = [
errorHeader,
if (error != null) error,
].join(' ');
throw ServerRequestFailedException(
message,
statusCode: response.statusCode,
responseContent: jsonMap,
);
}
return jsonMap;
}
Future<Map<String, dynamic>> oauthTokenRequest(
Map<String, String> postValues, {
required AuthEndpoints authEndpoints,
}) async {
final body = Stream<List<int>>.value(
ascii.encode(
postValues.entries
.map((e) => '${e.key}=${Uri.encodeComponent(e.value)}')
.join('&'),
),
);
final request = RequestImpl('POST', authEndpoints.tokenEndpoint, body)
..headers['content-type'] = _contentTypeUrlEncoded;
return requestJson(request, 'Failed to obtain access credentials.');
}
}
/// Returns an error string for [json] if it contains error data in keys
/// `error` and `error_description`.
///
/// Otherwise, returns `null`.
String? _errorStringFromJsonResponse(Map<String, dynamic> json) {
final error = json['error'];
final values = [
if (error != null) 'Error: $error',
json['error_description'],
].where((element) => element != null).join(' ');
if (values.isEmpty) return null;
return values;
}
Future<void> _expectJsonResponse(StreamedResponse response) async {
final contentType = response.headers['content-type'];
if (!_isJson(contentType)) {
String? body;
try {
body = await response.stream.bytesToString();
} catch (_) {
/// We're already going to throw below
}
final message = contentType == null
? 'Server responded without a content type header.'
: 'Server responded with invalid content type: $contentType. ';
throw ServerRequestFailedException(
'$message Expected a JSON response.',
statusCode: response.statusCode,
responseContent: body,
);
}
}
/// Follows https://mimesniff.spec.whatwg.org/#json-mime-type
bool _isJson(String? contentType) {
if (contentType == null) return false;
final mediaType = MediaType.parse(contentType);
if (mediaType.mimeType == 'application/json') return true;
if (mediaType.mimeType == 'text/json') return true;
return mediaType.subtype.endsWith('+json');
}
-17
View File
@@ -1,17 +0,0 @@
# See https://pub.dev/packages/mono_repo
sdk:
- pubspec
- dev
stages:
- analyze_and_format:
- group:
- format
- analyze: --fatal-infos .
sdk: dev
- group:
- analyze
sdk: pubspec
- unittest:
- test: -p vm
- test: -p chrome
-26
View File
@@ -1,26 +0,0 @@
name: googleapis_auth
version: 1.4.1
description: Obtain Access credentials for Google services using OAuth 2.0
repository: https://github.com/google/googleapis.dart/tree/master/googleapis_auth
environment:
sdk: '>=2.19.0 <3.0.0'
dependencies:
args: ^2.3.1
crypto: ^3.0.0
http: '>=0.13.5 <2.0.0'
http_parser: ^4.0.0
js: ^0.6.4
dev_dependencies:
# build_ dependencies allow debugging web tests using:
# `dart pub run build_runner serve test`
build_runner: ^2.0.0
build_test: ^2.0.0
build_web_compilers: '>=3.2.7 <5.0.0'
dart_flutter_team_lints: ^1.0.0
test: ^1.16.0
false_secrets:
- test/test_utils.dart
-131
View File
@@ -1,131 +0,0 @@
@TestOn('vm')
library googleapis_auth.adc_test;
import 'dart:convert';
import 'dart:io';
import 'package:googleapis_auth/auth_io.dart';
import 'package:googleapis_auth/src/adc_utils.dart'
show fromApplicationsCredentialsFile;
import 'package:googleapis_auth/src/known_uris.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';
import 'test_utils.dart';
void main() {
test('fromApplicationsCredentialsFile', () async {
final authEndpoints = GoogleAuthEndpoints();
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,
authEndpoints,
'test-credentials-file',
[],
mockClient((Request request) async {
final url = request.url;
if (url == googleOauth2TokenEndpoint) {
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.toString() ==
'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);
}),
);
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,
GoogleAuthEndpoints(),
'test-credentials-file',
[],
mockClient((Request request) async {
final url = request.url;
if (url == googleOauth2TokenEndpoint) {
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.toString() ==
'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);
}),
);
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);
}
});
}
-148
View File
@@ -1,148 +0,0 @@
// 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: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]);
});
}
-51
View File
@@ -1,51 +0,0 @@
// 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')));
});
});
}
@@ -1,88 +0,0 @@
// 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: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
]));
});
});
}
-57
View File
@@ -1,57 +0,0 @@
// 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 '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));
});
});
}
@@ -1,129 +0,0 @@
// 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: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(super.base, {required super.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')),
throwsArgumentError);
client.close();
});
});
test('non-closing-client', () {
final mock = mockClient(_defaultResponseHandler, expectClose: false);
nonClosingClient(mock).close();
});
});
}
-49
View File
@@ -1,49 +0,0 @@
// Copyright (c) 2021, 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:convert';
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:test/test.dart';
void main() {
test('AccessToken & AccessCredentials', () {
final credentials = AccessCredentials(
AccessToken('type', 'data', DateTime.now().toUtc()),
'refreshToken',
['scope1'],
idToken: 'idToken',
);
final encoded = jsonEncode(credentials);
final decoded = AccessCredentials.fromJson(
jsonDecode(encoded) as Map<String, dynamic>,
);
expect(decoded.refreshToken, 'refreshToken');
expect(decoded.idToken, 'idToken');
expect(decoded.scopes, ['scope1']);
expect(decoded.accessToken.expiry, credentials.accessToken.expiry);
expect(decoded.accessToken.data, credentials.accessToken.data);
expect(decoded.accessToken.type, credentials.accessToken.type);
expect(jsonEncode(decoded), encoded);
});
test('ClientId', () {
final clientId = ClientId('identifier', 'secret');
final encoded = jsonEncode(clientId);
final decoded = ClientId.fromJson(
jsonDecode(encoded) as Map<String, dynamic>,
);
expect(decoded.identifier, clientId.identifier);
expect(decoded.secret, clientId.secret);
expect(jsonEncode(decoded), encoded);
});
}
@@ -1,357 +0,0 @@
// 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:async';
import 'dart:convert';
import 'dart:io';
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:googleapis_auth/src/known_uris.dart';
import 'package:googleapis_auth/src/oauth2_flows/authorization_code_grant_manual_flow.dart';
import 'package:googleapis_auth/src/oauth2_flows/authorization_code_grant_server_flow.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';
import '../test_utils.dart';
typedef RequestHandler = Future<Response> Function(Request _);
final _browserFlowRedirectMatcher = predicate<String>((object) {
if (object.startsWith('redirect_uri=')) {
final url = Uri.parse(
Uri.decodeComponent(object.substring('redirect_uri='.length)));
expect(url.scheme, equals('http'));
expect(url.host, equals('localhost'));
return true;
}
return false;
});
void main() {
final clientId = ClientId('id', 'secret');
final scopes = ['s1', 's2'];
final authEndpoints = GoogleAuthEndpoints();
// Validation + Responses from the authorization server.
RequestHandler successFullResponse({required bool manual}) =>
(Request request) async {
expect(request.method, equals('POST'));
expect(request.url, googleOauth2TokenEndpoint);
expect(
request.headers['content-type']!,
startsWith('application/x-www-form-urlencoded'),
);
final pairs = request.body.split('&');
expect(pairs, hasLength(6));
expect(
pairs,
containsAll([
'grant_type=authorization_code',
'code=mycode',
'client_id=id',
'client_secret=secret',
allOf(
startsWith('code_verifier='),
hasLength(142), // happens to be the output length as implemented!
),
if (manual) 'redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob',
if (!manual) _browserFlowRedirectMatcher
]),
);
final result = {
'token_type': 'Bearer',
'access_token': 'tokendata',
'expires_in': 3600,
'refresh_token': 'my-refresh-token',
'id_token': 'my-id-token',
'scope': 's1 s2',
};
return Response(
jsonEncode(result),
200,
headers: jsonContentType,
);
};
Future<Response> invalidResponse(Request request) async {
// Missing expires_in field!
final result = {
'token_type': 'Bearer',
'access_token': 'tokendata',
'refresh_token': 'my-refresh-token',
'id_token': 'my-id-token',
};
return Response(jsonEncode(result), 200, headers: jsonContentType);
}
// 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, googleOauth2AuthorizationEndpoint.scheme);
expect(uri.authority, googleOauth2AuthorizationEndpoint.authority);
expect(uri.path, googleOauth2AuthorizationEndpoint.path);
expect(uri.queryParameters, {
'client_id': clientId.identifier,
'response_type': 'code',
'scope': 's1 s2',
'redirect_uri': isNotEmpty,
'code_challenge': hasLength(43),
'code_challenge_method': 'S256',
if (!manual) 'state': hasLength(32),
});
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) async {
validateUserPromptUri(url, manual: true);
return 'mycode';
}
test('successful', () async {
final flow = AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
mockClient(successFullResponse(manual: true), expectClose: false),
manualUserPrompt,
);
validateAccessCredentials(await flow.run());
});
test('user-exception', () async {
// We use a TransportException here for convenience.
Future<String> manualUserPromptError(String url) =>
Future.error(TransportException());
final flow = AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
mockClient(successFullResponse(manual: true), expectClose: false),
manualUserPromptError,
);
await expectLater(flow.run(), throwsA(isTransportException));
});
test('transport-exception', () async {
final flow = AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
transportFailure,
manualUserPrompt,
);
await expectLater(flow.run(), throwsA(isTransportException));
});
test('invalid-server-response', () async {
final flow = AuthorizationCodeGrantManualFlow(
authEndpoints,
clientId,
scopes,
mockClient(invalidResponse, expectClose: false),
manualUserPrompt,
);
await expectLater(flow.run(), throwsA(isServerRequestFailedException));
});
});
group('http-server', () {
Future<void> callRedirectionEndpoint(Uri authCodeCall) async {
final ioClient = HttpClient();
final closeMe = expectAsync0(ioClient.close);
try {
final request = await ioClient.getUrl(authCodeCall);
final response = await request.close();
await response.drain();
} finally {
closeMe();
}
}
Future<void> postToRedirectionEndpoint(Uri authCodeCall) async {
final ioClient = HttpClient();
final closeMe = expectAsync0(ioClient.close);
try {
final request = await ioClient.postUrl(authCodeCall);
final response = await request.close();
await response.drain();
} finally {
closeMe();
}
}
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 userPromptInvalidHttpVerb(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',
});
postToRedirectionEndpoint(authCodeCall);
}
void userPromptNonMatchingState(String url) {
final redirectUri = validateUserPromptUri(url);
final authCodeCall = Uri(
scheme: redirectUri.scheme,
host: redirectUri.host,
port: redirectUri.port,
path: redirectUri.path,
queryParameters: {
'state': 'not-the-right-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('successful', () async {
final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPrompt),
);
validateAccessCredentials(await flow.run());
});
test('transport-exception', () async {
final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
transportFailure,
expectAsync1(userPrompt),
);
await expectLater(flow.run(), throwsA(isTransportException));
});
test('non-GET request', () async {
final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPromptInvalidHttpVerb),
);
await expectLater(
flow.run,
throwsA(
isA<Exception>().having(
(e) => e.toString(),
'message',
'Exception: Invalid response from server (expected GET request callback, got: POST).',
),
),
);
});
test('request with invalid state parameter', () async {
final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPromptNonMatchingState),
);
await expectLater(
flow.run,
throwsA(
isA<Exception>().having(
(e) => e.toString(),
'message',
'Exception: Invalid response from server (state did not match).',
),
),
);
});
test('invalid-server-response', () async {
final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
mockClient(invalidResponse, expectClose: false),
expectAsync1(userPrompt),
);
await expectLater(flow.run(), throwsA(isServerRequestFailedException));
});
test('failed-authentication', () async {
final flow = AuthorizationCodeGrantServerFlow(
authEndpoints,
clientId,
scopes,
mockClient(successFullResponse(manual: false), expectClose: false),
expectAsync1(userPromptInvalidAuthCodeCallback),
);
await expectLater(flow.run(), throwsA(isUserConsentException));
});
}, testOn: '!browser');
});
}
@@ -1,59 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('error');
return dartFunction;
}
function GapiAuth() {}
GapiAuth.prototype.init = function(doneCallback) {
doneCallback();
};
GapiAuth.prototype.authorize = function(json, doneCallback) {
var client_id = json['client_id'];
var response_type = json['response_type'];
var scope = json['scope'];
var prompt = json['prompt'];
if (client_id == 'foo_client' &&
response_type == 'code token' &&
scope == 'scope1 scope2' &&
prompt == 'consent') {
doneCallback({
'token_type' : 'Bearer',
'access_token' : 'foo_token',
'expires_at' : Date.now() + 1000 * 3210,
'code' : 'mycode',
'scope': scope,
});
} else {
throw new Error('error');
}
};
// Initialize the gapi.auth mock.
window.gapi = new Object();
window.gapi.auth2 = new GapiAuth();
// Call the dart function. This signals that gapi.auth was loaded.
var dartFunction = findDartOnLoadCallback();
dartFunction();
})();
@@ -1,40 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
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));
final difference = credentials.accessToken.expiry.difference(date);
final seconds = difference.inSeconds;
expect(seconds, inInclusiveRange(-3, 3));
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');
});
}
@@ -1,57 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('error');
return dartFunction;
}
function GapiAuth() {}
GapiAuth.prototype.init = function(doneCallback) {
doneCallback();
};
GapiAuth.prototype.authorize = function(json, doneCallback) {
var client_id = json['client_id'];
var response_type = json['response_type'];
var scope = json['scope'];
if (client_id == 'foo_client' &&
response_type == 'code token' &&
scope == 'scope1 scope2') {
doneCallback({
'token_type' : 'Bearer',
'access_token' : 'foo_token',
'expires_at' : Date.now() + 1000 * 3210,
'code' : 'mycode',
'scope': scope,
});
} else {
throw new Error('error');
}
};
// Initialize the gapi.auth mock.
window.gapi = new Object();
window.gapi.auth2 = new GapiAuth();
// Call the dart function. This signals that gapi.auth was loaded.
var dartFunction = findDartOnLoadCallback();
dartFunction();
})();
@@ -1,39 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
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));
final difference = credentials.accessToken.expiry.difference(date);
final seconds = difference.inSeconds;
expect(seconds, inInclusiveRange(-3, 3));
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');
});
}
@@ -1,57 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('error');
return dartFunction;
}
function GapiAuth() {}
GapiAuth.prototype.init = function(doneCallback) {
doneCallback();
};
GapiAuth.prototype.authorize = function(json, doneCallback) {
var client_id = json['client_id'];
var response_type = json['response_type'];
var scope = json['scope'];
if (client_id == 'foo_client' &&
response_type == 'code token' &&
scope == 'scope1 scope2') {
doneCallback({
'token_type' : 'Bearer',
'access_token' : 'foo_token',
'expires_at' : Date.now() + 1000 * 3210,
'code' : 'mycode',
'scope': scope,
});
} else {
throw new Error('error');
}
};
// Initialize the gapi.auth mock.
window.gapi = new Object();
window.gapi.auth2 = new GapiAuth();
// Call the dart function. This signals that gapi.auth was loaded.
var dartFunction = findDartOnLoadCallback();
dartFunction();
})();
@@ -1,39 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
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));
final difference = credentials.accessToken.expiry.difference(date);
final seconds = difference.inSeconds;
expect(seconds, inInclusiveRange(-3, 3));
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');
});
}
@@ -1,56 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('error');
return dartFunction;
}
function GapiAuth() {}
GapiAuth.prototype.init = function(doneCallback) {
doneCallback();
};
GapiAuth.prototype.authorize = function(json, doneCallback) {
var client_id = json['client_id'];
var response_type = json['response_type'];
var scope = json['scope'];
if (client_id == 'foo_client' &&
response_type == 'token' &&
scope == 'scope1 scope2') {
doneCallback({
'token_type' : 'Bearer',
'access_token' : 'foo_token',
'expires_at' : Date.now() + 1000 * 3210,
'scope': scope,
});
} else {
throw new Error('error');
}
};
// Initialize the gapi.auth mock.
window.gapi = new Object();
window.gapi.auth2 = new GapiAuth();
// Call the dart function. This signals that gapi.auth was loaded.
var dartFunction = findDartOnLoadCallback();
dartFunction();
})();
@@ -1,36 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
final credentials =
await flow.obtainAccessCredentialsViaUserConsent(immediate: true);
final date = DateTime.now().toUtc().add(const Duration(seconds: 3210));
final difference = credentials.accessToken.expiry.difference(date);
final seconds = difference.inSeconds;
expect(seconds, inInclusiveRange(-3, 3));
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');
});
}
@@ -1,57 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('error');
return dartFunction;
}
function GapiAuth() { }
GapiAuth.prototype.init = function (doneCallback) {
doneCallback();
};
GapiAuth.prototype.authorize = function (json, doneCallback) {
var client_id = json['client_id'];
var response_type = json['response_type'];
var scope = json['scope'];
if (client_id == 'foo_client' &&
response_type == 'id_token token' &&
scope == 'scope1 scope2') {
doneCallback({
'token_type': 'Bearer',
'access_token': 'foo_token',
'id_token': 'foo_id_token',
'expires_at' : Date.now() + 1000 * 3210,
'scope': scope,
});
} else {
throw new Error('error');
}
};
// Initialize the gapi.auth mock.
window.gapi = new Object();
window.gapi.auth2 = new GapiAuth();
// Call the dart function. This signals that gapi.auth was loaded.
var dartFunction = findDartOnLoadCallback();
dartFunction();
})();
@@ -1,38 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
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));
final difference = credentials.accessToken.expiry.difference(date);
final seconds = difference.inSeconds;
expect(seconds, inInclusiveRange(-3, 3));
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');
});
}
@@ -1,56 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('error');
return dartFunction;
}
function GapiAuth() {}
GapiAuth.prototype.init = function(doneCallback) {
doneCallback();
};
GapiAuth.prototype.authorize = function(json, doneCallback) {
var client_id = json['client_id'];
var response_type = json['response_type'];
var scope = json['scope'];
if (client_id == 'foo_client' &&
response_type == 'token' &&
scope == 'scope1 scope2') {
doneCallback({
'token_type' : 'Bearer',
'access_token' : 'foo_token',
'expires_at' : Date.now() + 1000 * 3210,
'scope': scope,
});
} else {
throw new Error('error');
}
};
// Initialize the gapi.auth mock.
window.gapi = new Object();
window.gapi.auth2 = new GapiAuth();
// Call the dart function. This signals that gapi.auth was loaded.
var dartFunction = findDartOnLoadCallback();
dartFunction();
})();
@@ -1,36 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
final credentials = await flow.obtainAccessCredentialsViaUserConsent();
final date = DateTime.now().toUtc().add(const Duration(seconds: 3210));
final difference = credentials.accessToken.expiry.difference(date);
final seconds = difference.inSeconds;
expect(seconds, inInclusiveRange(-3, 3));
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');
});
}
@@ -1,53 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('error');
return dartFunction;
}
function GapiAuth() {}
GapiAuth.prototype.init = function(doneCallback) {
doneCallback();
};
GapiAuth.prototype.authorize = function(json, doneCallback) {
var client_id = json['client_id'];
var response_type = json['response_type'];
var scope = json['scope'];
if (client_id == 'foo_client' &&
response_type == 'token' &&
scope == 'scope1 scope2') {
doneCallback({
'error' : 'failed to get user consent',
});
} else {
throw new Error('error');
}
};
// Initialize the gapi.auth mock.
window.gapi = new Object();
window.gapi.auth2 = new GapiAuth();
// Call the dart function. This signals that gapi.auth was loaded.
var dartFunction = findDartOnLoadCallback();
dartFunction();
})();
@@ -1,30 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
final flow = await auth.createImplicitBrowserFlow(clientId, scopes);
try {
await flow.obtainAccessCredentialsViaUserConsent();
fail('expected error');
} catch (error) {
expect(error is auth.UserConsentException, isTrue);
}
});
}
@@ -1,37 +0,0 @@
// 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 new Error('error');
var callbackName = self.src.substring(equalsSign + 1);
if (callbackName.length <= 0) throw new Error('error');
var dartFunction = window[callbackName];
if (dartFunction == null) throw new Error('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();
})();
@@ -1,27 +0,0 @@
// 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')
library;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
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'];
// ignore: deprecated_member_use_from_same_package
expect(auth.createImplicitBrowserFlow(clientId, scopes), completes);
// ignore: deprecated_member_use_from_same_package
expect(auth.createImplicitBrowserFlow(clientId2, scopes), completes);
});
}
@@ -1,6 +0,0 @@
// 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
@@ -1,54 +0,0 @@
// 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')
@Timeout.factor(4)
library;
import 'dart:html';
import 'dart:js' as js;
import 'package:googleapis_auth/auth_browser.dart' as auth;
import 'package:googleapis_auth/src/browser_utils.dart' as browser_utils;
import 'package:googleapis_auth/src/oauth2_flows/implicit.dart' as impl;
import 'package:test/test.dart';
import 'utils.dart';
void main() {
test('gapi-load-failure', () {
impl.gapiUrl = resource('non_existent.js');
expect(
// ignore: deprecated_member_use_from_same_package
auth.createImplicitBrowserFlow(_clientId, _scopes),
throwsA(isA<auth.AuthenticationException>()),
);
});
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 {
// ignore: deprecated_member_use_from_same_package
await auth.createImplicitBrowserFlow(_clientId, _scopes);
fail('expected error');
} catch (error) {
final elapsed = (sw.elapsed - browser_utils.callbackTimeout).inSeconds;
expect(elapsed, inInclusiveRange(-3, 3));
}
});
}
final _clientId = auth.ClientId('a', 'b');
const _scopes = ['scope1', 'scope2'];
@@ -1,8 +0,0 @@
// 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();
@@ -1,94 +0,0 @@
// 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:async';
import 'dart:convert';
import 'package:googleapis_auth/src/known_uris.dart';
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() {
Future<Response> successfulSignRequest(Request request) async {
expect(request.method, equals('POST'));
expect(request.url, googleOauth2TokenEndpoint);
// 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 Response(body, 200, headers: jsonContentType);
}
Future<Response> invalidAccessToken(Request request) async {
final body = jsonEncode({
// Missing 'expires_in' entry
'access_token': 'atok',
'token_type': 'Bearer',
});
return Response(body, 200, headers: jsonContentType);
}
group('jwt-flow', () {
const clientEmail = 'a@b.com';
final scopes = ['s1', 's2'];
test('successful', () 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(isServerRequestFailedException));
});
test('transport-failure', () {
final flow =
JwtFlow(clientEmail, testPrivateKey, null, scopes, transportFailure);
expect(flow.run(), throwsA(isTransportException));
});
});
}
@@ -1,126 +0,0 @@
// 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.google.internal/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) async {
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 Response(body, 200, headers: jsonContentType);
}
Future<Response> invalidAccessToken(Request request) async {
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 Response(body, 200, headers: jsonContentType);
}
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('successful', () 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-response', () {
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(isServerRequestFailedException));
});
test('token-transport-error', () {
var requestNr = 0;
final flow = MetadataServerAuthorizationFlow(mockClient(
expectAsync1((request) {
if (requestNr++ == 0) {
// Dart 3 change that can't be fixed while we support Dart 2.x
// ignore: avoid_redundant_argument_values
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 {
// Dart 3 change that can't be fixed while we support Dart 2.x
// ignore: avoid_redundant_argument_values
return transportFailure.get(Uri.http('failure', ''));
}
}, count: 2),
expectClose: false));
expect(flow.run(), throwsA(isTransportException));
});
});
}
-403
View File
@@ -1,403 +0,0 @@
// 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:async';
import 'dart:convert';
import 'package:googleapis_auth/googleapis_auth.dart';
import 'package:googleapis_auth/src/http_client_base.dart';
import 'package:googleapis_auth/src/known_uris.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() {
final authEndpoints = GoogleAuthEndpoints();
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) async {
expect(request.method, equals('POST'));
expect(request.url, googleOauth2TokenEndpoint);
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);
}
Future<Response> refreshErrorResponse(Request request) async {
final body = jsonEncode({'error': 'An error occurred'});
return Response(body, 400, headers: jsonContentType);
}
Future<Response> serverError(Request request) =>
Future<Response>.error(Exception('transport layer exception'));
test('refreshCredentials-successful', () async {
final newCredentials = await refreshCredentials(
authEndpoints,
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 {
await expectLater(
refreshCredentials(
authEndpoints,
clientId,
credentials,
mockClient(serverError, expectClose: false),
),
throwsA(
isA<Exception>().having(
(p0) => p0.toString(),
'toString',
'Exception: transport layer exception',
),
),
);
});
test('refreshCredentials-error-response', () async {
await expectLater(
refreshCredentials(
authEndpoints,
clientId,
credentials,
mockClient(refreshErrorResponse, expectClose: false),
),
throwsA(isServerRequestFailedException),
);
});
group('authenticatedClient', () {
final url = Uri.parse('http://www.example.com');
test('successful', () async {
final client = authenticatedClient(
mockClient(expectAsync1((request) async {
expect(request.method, equals('POST'));
expect(request.url, equals(url));
expect(request.headers.length, equals(1));
expect(request.headers['Authorization'], equals('Bearer bar'));
return 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) async {
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 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(
authEndpoints,
clientId,
credentials,
mockClient(
expectAsync1((request) async => 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(
authEndpoints,
clientId,
credentials,
mockClient(_defaultResponseHandler, expectClose: false),
),
throwsA(isArgumentError),
);
});
test('refresh-failed', () {
final credentials = AccessCredentials(
AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1', 's2']);
final client = autoRefreshingClient(
authEndpoints,
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(isServerRequestFailedException));
});
test('invalid-content-type', () {
final credentials = AccessCredentials(
AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1', 's2']);
final client = autoRefreshingClient(
authEndpoints,
clientId,
credentials,
mockClient(expectAsync1((request) async {
// This should be a refresh request.
expect(request.headers['foo'], isNull);
final headers = {'content-type': 'image/png'};
return 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(isServerRequestFailedException));
});
test('successful-refresh', () async {
var serverInvocation = 0;
final credentials = AccessCredentials(
AccessToken('Bearer', 'bar', yesterday), 'refresh', ['s1']);
final client = autoRefreshingClient(
authEndpoints,
clientId,
credentials,
mockClient(
expectAsync1(
(request) async {
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 Response('', 200);
}
},
count: 2,
),
));
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();
});
});
});
}
@@ -1,37 +0,0 @@
import 'package:googleapis_auth/src/service_account_credentials.dart';
import 'package:test/test.dart';
void main() {
group(ServiceAccountCredentials, () {
group('fromJson', () {
test('throws exception if json is not a map', () {
expect(
() => ServiceAccountCredentials.fromJson('[1,2,3]'),
throwsArgumentError,
);
});
test('throws exception if json is not a service account', () {
expect(
() => ServiceAccountCredentials.fromJson({
'type': 'not_service_account',
'client_id': 'client_id',
'private_key': 'private_key',
'client_email': 'client_email',
}),
throwsArgumentError,
);
});
test('throws exception if json is missing fields', () {
expect(
() => ServiceAccountCredentials.fromJson({
'type': 'service_account',
'client_id': 'client_id',
}),
throwsArgumentError,
);
});
});
});
}
-87
View File
@@ -1,87 +0,0 @@
// 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 'package:googleapis_auth/googleapis_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 jsonContentType = {'content-type': 'application/json'};
const isServerRequestFailedException =
TypeMatcher<ServerRequestFailedException>();
const Matcher isUserConsentException = TypeMatcher<UserConsentException>();
const Matcher isAccessDeniedException = TypeMatcher<AccessDeniedException>();
const Matcher isTransportException = TypeMatcher<TransportException>();
class TransportException implements Exception {}
Client get transportFailure =>
MockClient(expectAsync1((_) async => throw 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(
MockClientHandler 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 void Function() _expectedToBeCalled;
ExpectCloseMockClient(
super.requestHandler,
int c,
) {
_expectedToBeCalled = expectAsync0(() {}, count: c);
}
@override
void close() {
super.close();
_expectedToBeCalled();
}
}
-111
View File
@@ -1,111 +0,0 @@
@Timeout(Duration(seconds: 2))
library;
import 'dart:convert';
import 'package:googleapis_auth/src/utils.dart';
import 'package:http/http.dart';
import 'package:test/test.dart';
import 'test_utils.dart';
void main() {
test('not valid UTF-8', () async {
const body = [
// https://man7.org/linux/man-pages/man7/utf-8.7.html
// 0xC0 is never used in UTF8-encoding!
0xC0,
];
final client = mockClient(
(request) async => Response.bytes(body, 200, headers: jsonContentType),
expectClose: false,
);
await expectLater(
client.requestJson(Request('GET', Uri.parse('localhost:8080')), 'bob'),
throwsA(
isServerRequestFailedException
.having(
(p0) => p0.message,
'message',
contains('The response was not valid UTF-8.'),
)
.having(
(p0) => p0.responseContent,
'responseContent',
body,
),
),
);
});
test('not JSON', () async {
const body = 'this is not good json!';
final client = mockClient(
(request) async => Response(body, 200, headers: jsonContentType),
expectClose: false,
);
await expectLater(
client.requestJson(Request('GET', Uri.parse('localhost:8080')), 'bob'),
throwsA(
isServerRequestFailedException
.having(
(p0) => p0.message,
'message',
contains('Could not decode the response as JSON.'),
)
.having(
(p0) => p0.responseContent,
'responseContent',
body,
),
),
);
});
test('not a map', () async {
final body = [];
final client = mockClient(
(request) async =>
Response(jsonEncode(body), 200, headers: jsonContentType),
expectClose: false,
);
await expectLater(
client.requestJson(Request('GET', Uri.parse('localhost:8080')), 'bob'),
throwsA(
isServerRequestFailedException
.having(
(p0) => p0.message,
'message',
'The returned JSON response was not a Map.',
)
.having(
(p0) => p0.responseContent,
'responseContent',
body,
),
),
);
});
test('invalid-server-status-code', () async {
final client = mockClient(
(request) async =>
Response(jsonEncode({}), 500, headers: jsonContentType),
expectClose: false,
);
await expectLater(
client.requestJson(Request('GET', Uri.parse('localhost:8080')), 'bob'),
throwsA(
isServerRequestFailedException.having(
(p0) => p0.statusCode,
'statusCode',
500,
),
),
);
});
}