blob: c99ec2853fd16f3212bffca9be9f33be9bc34272 [file] [edit]
// Copyright 2021 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart';
import 'auth_endpoints.dart';
import 'auth_functions.dart';
import 'auth_http_utils.dart';
import 'impersonated_auth_client.dart';
import 'service_account_client.dart';
import 'service_account_credentials.dart';
import 'sts_auth_client.dart';
Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
File file,
String fileSource,
List<String> scopes,
Client baseClient,
) async {
Object? credentials;
try {
credentials = json.decode(await file.readAsString());
} on IOException catch (e) {
throw CredentialsFileException(
'Failed to read credentials file from $fileSource',
e,
);
} on FormatException catch (e) {
throw CredentialsFileException(
'Failed to parse JSON from credentials file from $fileSource',
e,
);
}
if (credentials is! Map) {
throw CredentialsFileException(
'Failed to parse JSON from credentials file from $fileSource',
);
}
return _clientViaApplicationCredentials(
credentials as Map<String, dynamic>,
scopes,
baseClient,
fileSource: fileSource,
);
}
Future<AutoRefreshingAuthClient> _clientViaApplicationCredentials(
Map<String, dynamic> credentials,
List<String> scopes,
Client baseClient, {
String? fileSource,
}) async {
final quotaProject = credentials['quota_project_id'] as String?;
if (credentials case {
'type': 'authorized_user',
'client_id': final String clientIdString,
'client_secret': final String? clientSecret,
'refresh_token': final String? refreshToken,
}) {
if (refreshToken == null) {
throw CredentialsFileException(
'Failed to parse JSON from credentials file from '
'${fileSource ?? 'the provided credentials'}'
': refresh_token is missing.',
);
}
final clientId = ClientId(clientIdString, clientSecret);
return AutoRefreshingClient(
baseClient,
const GoogleAuthEndpoints(),
clientId,
await refreshCredentials(
clientId,
AccessCredentials(
// Deliberately expired — forces a token exchange immediately.
AccessToken(
'Bearer',
'',
DateTime.fromMillisecondsSinceEpoch(0, isUtc: true),
),
refreshToken,
scopes,
),
baseClient,
),
quotaProject: quotaProject,
);
}
if (credentials case {
'type': 'impersonated_service_account',
'service_account_impersonation_url': final String url,
'source_credentials': final Map<String, dynamic> source,
}) {
final sourceClient = await _clientViaApplicationCredentials(
source,
['https://www.googleapis.com/auth/iam'],
baseClient,
fileSource: fileSource,
);
final match = _impersonationUrlRegExp.firstMatch(url);
if (match == null) {
throw ArgumentError.value(
url,
'service_account_impersonation_url',
'Invalid impersonation URL',
);
}
final targetServiceAccount = match.group(1)!;
return clientViaServiceAccountImpersonation(
sourceClient: sourceClient,
targetServiceAccount: targetServiceAccount,
targetScopes: scopes,
baseClient: baseClient,
);
}
if (credentials case {
'type': 'external_account',
'audience': final String audience,
'subject_token_type': final String subjectTokenType,
'token_url': final String tokenUrl,
'credential_source': final Map<String, dynamic> credentialSource,
}) {
final serviceAccountImpersonationUrl =
credentials['service_account_impersonation_url'] as String?;
final stsClient = await clientViaStsTokenExchange(
credentialSource: credentialSource,
audience: audience,
subjectTokenType: subjectTokenType,
tokenUrl: tokenUrl,
scopes: scopes,
quotaProject: credentials['quota_project_id'] as String?,
baseClient: baseClient,
);
if (serviceAccountImpersonationUrl != null) {
// It's possible for external credentials to specify a service account
// to impersonate. This is common in Workload Identity Federation where
// the external identity (e.g. AWS, Azure, OIDC) is first exchanged for
// an STS token, which is then used to impersonate a specific Google Cloud
// service account.
//
// See: https://cloud.google.com/iam/docs/workload-identity-federation
// See also the "service_account_impersonation_url" definition at:
// https://google.aip.dev/auth/4117
final match = _impersonationUrlRegExp.firstMatch(
serviceAccountImpersonationUrl,
);
if (match == null) {
throw ArgumentError.value(
serviceAccountImpersonationUrl,
'service_account_impersonation_url',
'Invalid impersonation URL',
);
}
final targetServiceAccount = match.group(1)!;
return clientViaServiceAccountImpersonation(
sourceClient: stsClient,
targetServiceAccount: targetServiceAccount,
targetScopes: scopes,
baseClient: baseClient,
);
}
return stsClient;
}
return await clientViaServiceAccount(
ServiceAccountCredentials.fromJson(credentials),
scopes,
baseClient: baseClient,
quotaProject: quotaProject,
);
}
/// Matches the target service account email from a service account
/// impersonation URL.
///
/// Example URL:
/// `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SA_NAME@PROJECT.iam.gserviceaccount.com:generateAccessToken`
///
/// See:
/// https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
final _impersonationUrlRegExp = RegExp(
r'serviceAccounts/([^:]+):generateAccessToken',
);