From c820d428919be0ca67b61dd0af967f4c4d2b0589 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Wed, 22 Apr 2026 23:26:01 +0000 Subject: [PATCH] fix(idtoken): avoid double impersonation in tokenSourceFromBytes This PR fixes a parallel double impersonation bug in the `idtoken` package. The library incorrectly does not use the `source_credentials` subfield in the JSON struct when constructing the inner client, and instead passes the entire credential JSON. This causes the lower layers (`htransport.NewClient`) to correctly (but unexpectedly for this context) build an authenticated HTTP client that is already impersonated, leading to self-impersonation when calling `generateIdToken`. This PR fixes the issue by extracting or recreating non-impersonated credentials before calling `impersonate.IDTokenSource`, avoiding the double wrap. Note: This PR does not add new unit tests for the call sequence because `impersonate.IDTokenSource` hardcodes the IAM credentials endpoint, making it impossible to intercept with a mock client or server without modifying that package. The existing unit tests in this package only cover type validation and do not successfully execute the full impersonation flow due to this same limitation. closes: #2301 --- idtoken/idtoken.go | 50 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/idtoken/idtoken.go b/idtoken/idtoken.go index f2eb7c5ddf..a6e2e59970 100644 --- a/idtoken/idtoken.go +++ b/idtoken/idtoken.go @@ -231,7 +231,19 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds TargetPrincipal: account, IncludeEmail: true, } - ts, err := impersonate.IDTokenSource(ctx, config, option.WithAuthCredentialsJSON(credType, data)) + + baseData, err := baseDataForImpersonation(credType, data) + if err != nil { + return nil, err + } + + var opts []option.ClientOption + opts = append(opts, option.WithCredentialsJSON(baseData)) + if ds.HTTPClient != nil { + opts = append(opts, option.WithHTTPClient(ds.HTTPClient)) + } + + ts, err := impersonate.IDTokenSource(ctx, config, opts...) if err != nil { return nil, err } @@ -241,6 +253,42 @@ func tokenSourceFromBytes(ctx context.Context, data []byte, audience string, ds } } +// baseDataForImpersonation extracts or recreates non-impersonated credentials +// from the provided JSON data to avoid double impersonation. The problem is +// that passing the entire credential JSON causes the lower layers to +// automatically build an authenticated HTTP client that is already impersonated. +// To fix this, we extract the non-impersonated source credentials or remove the +// impersonation instructions before creating the client. This avoids leaky +// abstractions and respects the separation of concerns by letting the lower +// layers act as general loaders while handling the specific needs of idtoken +// generation here. +func baseDataForImpersonation(credType credentialstype.CredType, data []byte) ([]byte, error) { + var baseData []byte + if credType == ImpersonatedServiceAccount { + type source struct { + SourceCredentials json.RawMessage `json:"source_credentials"` + } + var s source + if err := json.Unmarshal(data, &s); err != nil { + return nil, err + } + baseData = s.SourceCredentials + } else { + // For ExternalAccount, we remove the service_account_impersonation_url + var m map[string]interface{} + if err := json.Unmarshal(data, &m); err != nil { + return nil, err + } + delete(m, "service_account_impersonation_url") + var err error + baseData, err = json.Marshal(m) + if err != nil { + return nil, err + } + } + return baseData, nil +} + // WithCustomClaims optionally specifies custom private claims for an ID token. func WithCustomClaims(customClaims map[string]interface{}) ClientOption { return withCustomClaims(customClaims)