refactor: remove fork of googleauth_apis (#1760)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
.dart_tool/
|
||||
.packages
|
||||
pubspec.lock
|
||||
|
||||
# User files
|
||||
test_integration/*.json
|
||||
test_integration/client_id*.yaml
|
||||
test_integration/config.yaml
|
||||
-193
@@ -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.
|
||||
Vendored
-27
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Vendored
-57
@@ -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,
|
||||
);
|
||||
}
|
||||
-57
@@ -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';
|
||||
-128
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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]);
|
||||
});
|
||||
}
|
||||
@@ -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
|
||||
]));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
}
|
||||
-59
@@ -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();
|
||||
})();
|
||||
-40
@@ -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');
|
||||
});
|
||||
}
|
||||
-57
@@ -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();
|
||||
})();
|
||||
Vendored
-39
@@ -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');
|
||||
});
|
||||
}
|
||||
-57
@@ -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();
|
||||
})();
|
||||
-39
@@ -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');
|
||||
});
|
||||
}
|
||||
-56
@@ -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();
|
||||
})();
|
||||
-36
@@ -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');
|
||||
});
|
||||
}
|
||||
-57
@@ -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();
|
||||
})();
|
||||
Vendored
-38
@@ -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();
|
||||
})();
|
||||
-36
@@ -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');
|
||||
});
|
||||
}
|
||||
-53
@@ -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();
|
||||
})();
|
||||
-30
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
-37
@@ -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();
|
||||
})();
|
||||
Vendored
-27
@@ -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
|
||||
-54
@@ -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
@@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user