Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions googleapis_auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
## 2.1.0-beta.1
- Add `serviceAccountCredentials` getter to AuthClient
- Added parsing for project_id and universe_domain properties for ServiceAccountCredentials
- Add `sign()` method to `ServiceAccountCredentials` for RSA-SHA256 signing
- Add `IAMSigner` class for signing via IAM Credentials API
- Add `clientViaServiceAccountImpersonation()` function and `ImpersonatedAuthClient` class for service account impersonation via IAM Credentials API
- Add `AuthClientSigningExtension` extension on `AuthClient` providing a universal `sign()` method that works across all auth contexts (service accounts, ADC, impersonated credentials)
- Require `meta: ^1.0.2`
- Require `sdk: ^3.9.0`
- Drop unneeded `args` dependency.
## 2.1.0-wip

- `AuthClient`
- Added `serviceAccountCredentials` getter.
- Added `sign()` method via `AuthClientSigningExtension`.
- `ServiceAccountCredentials`
- Added parsing for `projectId` and `universeDomain` properties.
- Added `sign()` method for RSA-SHA256 signing.
- `IAMSigner`
- Now uses the unified metadata cache from `package:google_cloud`.
- `MetadataServerAuthorizationFlow`
- Now uses `getMetadataValue` (caching) and `fetchMetadataValue`
(non-caching) from `package:google_cloud`.
- Added `refresh` support to `run()`.
- Added `clientViaServiceAccountImpersonation()` function and
`ImpersonatedAuthClient` class for service account impersonation via IAM
Credentials API.
- Export `RSAPrivateKey` which is exposed by `ServiceAccountCredentials`.
- Modernized code using pattern matching and switch expressions.
- Require `google_cloud: ^0.3.0`.
- Require `meta: ^1.15.0`.
- Require `sdk: ^3.9.0`.
- Drop unneeded `args` dependency.

## 2.0.0

Expand Down
14 changes: 8 additions & 6 deletions googleapis_auth/lib/src/adc_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
);
}

if (credentials is Map && credentials['type'] == 'authorized_user') {
final clientId = ClientId(
credentials['client_id'] as String,
credentials['client_secret'] as String?,
);
if (credentials case {
'type': 'authorized_user',
'client_id': final String clientIdString,
'client_secret': final String? clientSecret,
'refresh_token': final String? refreshToken,
}) {
final clientId = ClientId(clientIdString, clientSecret);
return AutoRefreshingClient(
baseClient,
const GoogleAuthEndpoints(),
Expand All @@ -45,7 +47,7 @@ Future<AutoRefreshingAuthClient> fromApplicationsCredentialsFile(
AccessCredentials(
// Hack: Create empty credentials that have expired.
AccessToken('Bearer', '', DateTime(0).toUtc()),
credentials['refresh_token'] as String?,
refreshToken,
scopes,
),
baseClient,
Expand Down
27 changes: 23 additions & 4 deletions googleapis_auth/lib/src/auth_client_signing_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// 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.

/// @docImport 'auth_client.dart';
library;

import 'dart:convert';

import 'auth_client.dart';
import 'iam_signer.dart';
import 'impersonated_auth_client.dart';
import 'service_account_credentials.dart';
import 'utils.dart';

/// Extension providing smart signing capabilities for [AuthClient].
Expand Down Expand Up @@ -38,6 +41,22 @@ import 'utils.dart';
/// final signature = await client.sign(utf8.encode('data to sign'));
/// ```
extension AuthClientSigningExtension on AuthClient {
/// Returns the service account email associated with this client.
///
/// If the client was created with explicit [ServiceAccountCredentials],
/// returns the email from those credentials.
///
/// Otherwise, queries the GCE metadata server to retrieve the default
/// service account email.
///
/// The result is cached for the lifetime of the Dart process by the
/// underlying [IAMSigner].
///
/// If [refresh] is `true`, the cache is cleared and the value is re-computed.
Future<String> getServiceAccountEmail({bool refresh = false}) async =>
serviceAccountCredentials?.email ??
await IAMSigner(this).getServiceAccountEmail(refresh: refresh);

/// Signs some bytes using the credentials from this auth client.
///
/// The signing behavior depends on the auth client type:
Expand Down Expand Up @@ -65,13 +84,13 @@ extension AuthClientSigningExtension on AuthClient {
/// final client = await clientViaServiceAccount(credentials, scopes);
/// final data = utf8.encode('data to sign');
/// final signature = await client.sign(data);
/// print('Signature (base64): $signature');
/// print('Signature (base64): ${signature.signedBlob}');
/// ```
Future<String> sign(List<int> data, {String? endpoint}) async {
// Check if this is an impersonated client
if (this is ImpersonatedAuthClient) {
final impersonated = this as ImpersonatedAuthClient;
return impersonated.sign(data);
return (await impersonated.sign(data)).signedBlob;
}

// Check if we have service account credentials for local signing
Expand All @@ -86,6 +105,6 @@ extension AuthClientSigningExtension on AuthClient {
final universeDomain =
serviceAccountCreds?.universeDomain ?? defaultUniverseDomain;
endpoint ??= 'https://iamcredentials.$universeDomain';
return IAMSigner(this, endpoint: endpoint).sign(data);
return (await IAMSigner(this, endpoint: endpoint).sign(data)).signedBlob;
}
}
134 changes: 77 additions & 57 deletions googleapis_auth/lib/src/iam_signer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

import 'package:google_cloud/general.dart';
import 'package:http/http.dart' as http;

import 'exceptions.dart';
import 'utils.dart';

/// Signs data using the IAM Credentials API's signBlob endpoint.
Expand Down Expand Up @@ -44,10 +42,8 @@ import 'utils.dart';
/// ```
class IAMSigner {
final http.Client _client;
final String? _serviceAccountEmail;
final String _endpoint;

String? _cachedEmail;
final String? serviceAccountEmail;
final String endpoint;

/// Creates an [IAMSigner] instance.
///
Expand All @@ -58,81 +54,105 @@ class IAMSigner {
/// signing. If not provided, it will be fetched from the GCE metadata server.
///
/// [universeDomain] specifies the universe domain for constructing the IAM
/// endpoint. Defaults to [defaultUniverseDomain] (googleapis.com).
/// endpoint. Defaults to [defaultUniverseDomain].
///
/// [endpoint] specifies a custom IAM Credentials API endpoint URL.
/// If provided, takes precedence over [universeDomain].
IAMSigner(
http.Client client, {
String? serviceAccountEmail,
this.serviceAccountEmail,
String? endpoint,
String universeDomain = defaultUniverseDomain,
}) : _client = client,
_serviceAccountEmail = serviceAccountEmail,
_endpoint = endpoint ?? 'https://iamcredentials.$universeDomain';
endpoint = endpoint ?? 'https://iamcredentials.$universeDomain';

/// Returns the service account email.
///
/// If an email was provided in the constructor, returns that email.
/// If an [serviceAccountEmail] was provided in the constructor, returns that
/// email.
/// Otherwise, queries the GCE metadata server to retrieve the default
/// service account email.
Future<String> getServiceAccountEmail() async {
if (_serviceAccountEmail != null) {
return _serviceAccountEmail;
}

if (_cachedEmail != null) {
return _cachedEmail!;
}

final metadataHost =
Platform.environment[gceMetadataHostEnvVar] ?? defaultMetadataHost;
final emailUrl = Uri.parse(
'http://$metadataHost/computeMetadata/v1/instance/service-accounts/default/email',
);

final response = await _client.get(emailUrl, headers: metadataFlavorHeader);
if (response.statusCode != 200) {
throw ServerRequestFailedException(
'Failed to get service account email from metadata server.',
statusCode: response.statusCode,
responseContent: response.body,
///
/// The result is cached for the lifetime of the Dart process.
/// Subsequent calls return the cached value without performing discovery
/// again.
///
/// If [refresh] is `true`, the cache is cleared and the value is re-computed.
///
/// If the metadata server cannot be contacted or returns a non-200 status
/// code, a [MetadataServerException] is thrown.
Future<String> getServiceAccountEmail({bool refresh = false}) async =>
serviceAccountEmail ??
await serviceAccountEmailFromMetadataServer(
client: _client,
refresh: refresh,
);
}

_cachedEmail = response.body.trim();
return _cachedEmail!;
}

/// Signs the given [data] using the IAM Credentials API.
///
/// Returns the signature as a String (base64-encoded).
/// Returns a record containing the signature as a base64-encoded String and
/// the key ID used to sign the blob.
///
/// If [refresh] is `true`, the service account email cache is cleared and
/// re-computed before signing.
///
/// [delegates] specifies the sequence of service accounts in a delegation
/// chain.
///
/// If [serviceAccountEmail] is not set, [getServiceAccountEmail] is called.
///
/// Throws a [ServerRequestFailedException] if the signing operation fails.
Future<String> sign(List<int> data) async {
final email = await getServiceAccountEmail();
/// Each service account must be granted the `roles/iam.serviceAccountTokenCreator`
/// role on its next service account in the chain. The last service account in
/// the chain must be granted the `roles/iam.serviceAccountTokenCreator` role
/// on the service account that is specified by [serviceAccountEmail] (or the
/// discovered service account email).
///
/// Throws [http.ClientException] if the signing operation fails.
Future<({String signedBlob, String keyId})> sign(
List<int> data, {
bool refresh = false,
List<String>? delegates,
}) async {
final email =
serviceAccountEmail ?? await getServiceAccountEmail(refresh: refresh);
final encodedEmail = Uri.encodeComponent(email);

final signBlobUrl = Uri.parse(
'$_endpoint/v1/projects/-/serviceAccounts/$encodedEmail:signBlob',
'$endpoint/v1/projects/-/serviceAccounts/$encodedEmail:signBlob',
);

final requestBody = jsonEncode({'payload': base64Encode(data)});
final request = http.Request('POST', signBlobUrl)
..headers['Content-Type'] = 'application/json'
..body = requestBody;
final requestBody = jsonEncode({
'payload': base64Encode(data),
'delegates': ?delegates,
});

final responseJson = await _client.requestJson(
request,
'Failed to sign blob via IAM.',
final response = await _client.post(
signBlobUrl,
headers: {'Content-Type': 'application/json'},
body: requestBody,
);

return switch (responseJson) {
{'signedBlob': final String signedBlob} => signedBlob,
_ => throw ServerRequestFailedException(
'IAM signBlob response missing signedBlob field.',
responseContent: responseJson,
),
};
if (response.statusCode != 200) {
throw http.ClientException(
'Failed to sign blob via IAM. '
'Status: ${response.statusCode}, Body: ${response.body}',
signBlobUrl,
);
}

final responseJson = jsonDecode(response.body) as Map<String, dynamic>;

if (responseJson case {
'signedBlob': final String signedBlob,
'keyId': final String keyId,
}) {
return (signedBlob: signedBlob, keyId: keyId);
}

throw http.ClientException(
'IAM signBlob response missing signedBlob or keyId field. '
'Body: ${response.body}',
signBlobUrl,
);
}
}
14 changes: 7 additions & 7 deletions googleapis_auth/lib/src/impersonated_auth_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,12 @@ class ImpersonatedAuthClient extends AutoRefreshDelegatingClient {
'lifetime': '${_lifetime.inSeconds}s',
});

final request = http.Request('POST', tokenUrl)
..headers['Content-Type'] = 'application/json'
..body = requestBody;

final responseJson = await _sourceClient.requestJson(
request,
'POST',
tokenUrl,
'Failed to generate access token for impersonated service account.',
headers: {'Content-Type': 'application/json'},
body: requestBody,
);

final (accessToken, expireTime) = switch (responseJson) {
Expand All @@ -192,12 +191,13 @@ class ImpersonatedAuthClient extends AutoRefreshDelegatingClient {
/// This method calls the IAM Credentials API signBlob endpoint to sign data
/// as the impersonated service account.
///
/// Returns the signature as a String
/// Returns a record containing the signature as a base64-encoded String and
/// the key ID used to sign the blob.
///
/// Throws [ServerRequestFailedException] if the signing operation fails.
///
/// See: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob
Future<String> sign(List<int> data) {
Future<({String signedBlob, String keyId})> sign(List<int> data) {
final signer = IAMSigner(
_sourceClient,
serviceAccountEmail: _targetServiceAccount,
Expand Down
Loading
Loading