340 lines
12 KiB
Dart
340 lines
12 KiB
Dart
// 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;
|
|
}
|