[googleapis_auth] feat: Add ImpersonatedAuthClient for service account impersonation#711
Conversation
PR HealthLicense Headers ✔️
All source files should start with a license header. This check can be disabled by tagging the PR with API leaks ✔️The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.
This check can be disabled by tagging the PR with Breaking changes ✔️
This check can be disabled by tagging the PR with Unused Dependencies ✔️
For details on how to fix these, see dependency_validator. This check can be disabled by tagging the PR with Changelog Entry ✔️
Changes to files need to be accounted for in their respective changelogs. This check can be disabled by tagging the PR with |
…a IAM Credentials API
b2edf67 to
4772852
Compare
� Conflicts: � googleapis_auth/CHANGELOG.md
| final AuthClient _sourceClient; | ||
| final String _targetServiceAccount; | ||
| final List<String> _targetScopes; | ||
| final List<String>? _delegates; | ||
| final String _universeDomain; | ||
| final Duration _lifetime; | ||
|
|
||
| AccessCredentials _credentials; | ||
| http.Client? _authClient; |
There was a problem hiding this comment.
I LOVE your code organization.
final bits grouped.
Then mutable bits.
Kudos! 🤘
| }) : _sourceClient = sourceClient, | ||
| _targetServiceAccount = targetServiceAccount, | ||
| _targetScopes = List.unmodifiable(targetScopes), | ||
| _delegates = delegates != null ? List.unmodifiable(delegates) : null, |
There was a problem hiding this comment.
you're AMAZING. never trust the list to not be mutated. Again, kudos.
|
Here are Gemini's suggested fixes diff --git a/googleapis_auth/lib/src/iam_signer.dart b/googleapis_auth/lib/src/iam_signer.dart
index bfbaaae36..19405b540 100644
--- a/googleapis_auth/lib/src/iam_signer.dart
+++ b/googleapis_auth/lib/src/iam_signer.dart
@@ -62,10 +62,11 @@ class IAMSigner {
IAMSigner(
http.Client client, {
String? serviceAccountEmail,
- String endpoint = 'https://iamcredentials.$defaultUniverseDomain',
+ String? endpoint,
+ String universeDomain = defaultUniverseDomain,
}) : _client = client,
_serviceAccountEmail = serviceAccountEmail,
- _endpoint = endpoint;
+ _endpoint = endpoint ?? 'https://iamcredentials.$universeDomain';
/// Returns the service account email.
///
diff --git a/googleapis_auth/lib/src/impersonated_auth_client.dart b/googleapis_auth/lib/src/impersonated_auth_client.dart
index 6ea8d265e..c3295b8e1 100644
--- a/googleapis_auth/lib/src/impersonated_auth_client.dart
+++ b/googleapis_auth/lib/src/impersonated_auth_client.dart
@@ -169,15 +169,13 @@ class ImpersonatedAuthClient extends AutoRefreshDelegatingClient {
'Failed to generate access token for impersonated service account.',
);
- final accessToken = responseJson['accessToken'] as String?;
- final expireTime = responseJson['expireTime'] as String?;
-
- if (accessToken == null || expireTime == null) {
- throw ServerRequestFailedException(
+ final (accessToken, expireTime) = switch (responseJson) {
+ {'accessToken': final String t, 'expireTime': final String e} => (t, e),
+ _ => throw ServerRequestFailedException(
'IAM generateAccessToken response missing required fields.',
responseContent: responseJson,
- );
- }
+ ),
+ };
// Parse RFC 3339 timestamp
final expiry = DateTime.parse(expireTime);
@@ -203,7 +201,7 @@ class ImpersonatedAuthClient extends AutoRefreshDelegatingClient {
final signer = IAMSigner(
_sourceClient,
serviceAccountEmail: _targetServiceAccount,
- endpoint: _universeDomain,
+ universeDomain: _universeDomain,
);
return signer.sign(data);
}
diff --git a/googleapis_auth/test/iam_signer_test.dart b/googleapis_auth/test/iam_signer_test.dart
index 1e067bf9c..4c5747908 100644
--- a/googleapis_auth/test/iam_signer_test.dart
+++ b/googleapis_auth/test/iam_signer_test.dart
@@ -161,6 +161,39 @@ void main() {
expect(signature, equals(base64Encode([99, 88, 77])));
});
+ test('sign with custom universe domain', () async {
+ final mockClient = MockClient(
+ expectAsync1((request) async {
+ if (request.method == 'POST') {
+ expect(
+ request.url.toString(),
+ equals(
+ 'https://iamcredentials.example.com/v1/projects/-/serviceAccounts/custom%40example.iam.gserviceaccount.com:signBlob',
+ ),
+ );
+
+ return http.Response(
+ jsonEncode({
+ 'signedBlob': base64Encode([11, 22, 33]),
+ }),
+ 200,
+ headers: {'content-type': 'application/json'},
+ );
+ }
+ fail('Unexpected request: ${request.method} ${request.url}');
+ }),
+ );
+
+ final signer = IAMSigner(
+ mockClient,
+ serviceAccountEmail: 'custom@example.iam.gserviceaccount.com',
+ universeDomain: 'example.com',
+ );
+
+ final signature = await signer.sign([5, 6, 7]);
+ expect(signature, equals(base64Encode([11, 22, 33])));
+ });
+
test('sign with custom endpoint', () async {
final mockClient = MockClient(
expectAsync1((request) async { |
I was just fixing this. Nice! |
Co-authored-by: Kevin Moore <kevmoo@users.noreply.github.com>

Implements service account impersonation via the IAM Credentials API, allowing a source credential to act as a different service account.
Changes
Added
clientViaServiceAccountImpersonation()function andImpersonatedAuthClientclass:Issue 158 (dart_firebase_admin)