From 5b53c7a52f9fd025385c454725172e0c2ef6ba30 Mon Sep 17 00:00:00 2001 From: Russ Taylor Date: Mon, 1 Jun 2026 13:57:51 -0700 Subject: [PATCH] fix: fallback to GCP metadata if authorized_user credentials Adds a fallback to the GCP tokensource when encountering a `unsupported credentials type: authorized_user` error. This often indicates that a user has manually run `gcloud auth application-default login`. Unfortunately this caused an immediate failure with no fallback. This adds an explicit fallback to the GCP metadata server in case it encounters an issue from this exact case. --- internal/client/tokensource/gcp.go | 37 ++++++++++- internal/client/tokensource/gcp_test.go | 82 +++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 internal/client/tokensource/gcp_test.go diff --git a/internal/client/tokensource/gcp.go b/internal/client/tokensource/gcp.go index 9c37c61..c059d1b 100644 --- a/internal/client/tokensource/gcp.go +++ b/internal/client/tokensource/gcp.go @@ -3,17 +3,29 @@ package tokensource import ( "context" "fmt" + "net/url" + "strings" + "cloud.google.com/go/compute/metadata" "google.golang.org/api/idtoken" ) +var ( + newIDTokenSource = idtoken.NewTokenSource + metadataGet = metadata.GetWithContext +) + // FromGCP fetches an OIDC identity token using GCP Application Default // Credentials. This works on GCE, Cloud Run, GKE, and anywhere a service // account key or workload identity federation is configured. func FromGCP(ctx context.Context, audience string) (string, error) { - ts, err := idtoken.NewTokenSource(ctx, audience) + ts, err := newIDTokenSource(ctx, audience) if err != nil { + if isUnsupportedAuthorizedUser(err) { + return fromGCPMetadata(ctx, audience) + } + return "", fmt.Errorf("creating GCP token source: %w", err) } @@ -30,3 +42,26 @@ func FromGCP(ctx context.Context, audience string) (string, error) { return token.AccessToken, nil } + +func isUnsupportedAuthorizedUser(err error) bool { + errString := err.Error() + return strings.Contains(errString, "unsupported credentials type") && strings.Contains(errString, "authorized_user") +} + +func fromGCPMetadata(ctx context.Context, audience string) (string, error) { + v := url.Values{} + v.Set("audience", audience) + v.Set("format", "full") + + token, err := metadataGet(ctx, "instance/service-accounts/default/identity?"+v.Encode()) + + if err != nil { + return "", fmt.Errorf("fetching GCP identity token from metadata server: %w", err) + } + + if token == "" { + return "", fmt.Errorf("GCP metadata server returned an empty token") + } + + return token, nil +} diff --git a/internal/client/tokensource/gcp_test.go b/internal/client/tokensource/gcp_test.go new file mode 100644 index 0000000..21b092e --- /dev/null +++ b/internal/client/tokensource/gcp_test.go @@ -0,0 +1,82 @@ +package tokensource + +import ( + "context" + "errors" + "testing" + + "golang.org/x/oauth2" + "google.golang.org/api/idtoken" +) + +func TestFromGCPFallsBackToMetadataForAuthorizedUserCredentials(t *testing.T) { + tests := []struct { + name string + err string + }{ + {"unquoted", "idtoken: unsupported credentials type: authorized_user"}, + {"quoted", "idtoken: unsupported credentials type: \"authorized_user\""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetGCPTestHooks(t) + + newIDTokenSource = func(_ context.Context, _ string, _ ...idtoken.ClientOption) (oauth2.TokenSource, error) { + return nil, errors.New(tt.err) + } + + var gotSuffix string + metadataGet = func(_ context.Context, suffix string) (string, error) { + gotSuffix = suffix + return "metadata-token", nil + } + + token, err := FromGCP(context.Background(), "https://sts.example/token") + + if err != nil { + t.Fatalf("FromGCP returned error: %v", err) + } + + if token != "metadata-token" { + t.Fatalf("FromGCP token = %q, want %q", token, "metadata-token") + } + + wantSuffix := "instance/service-accounts/default/identity?audience=https%3A%2F%2Fsts.example%2Ftoken&format=full" + if gotSuffix != wantSuffix { + t.Fatalf("metadata suffix = %q, want %q", gotSuffix, wantSuffix) + } + }) + } +} + +func TestFromGCPDoesNotFallBackForOtherTokenSourceErrors(t *testing.T) { + resetGCPTestHooks(t) + + newIDTokenSource = func(_ context.Context, _ string, _ ...idtoken.ClientOption) (oauth2.TokenSource, error) { + return nil, errors.New("boom") + } + + metadataGet = func(_ context.Context, _ string) (string, error) { + t.Fatal("metadataGet should not be called") + return "", nil + } + + _, err := FromGCP(context.Background(), "https://sts.example/token") + + if err == nil { + t.Fatal("FromGCP returned nil error") + } +} + +func resetGCPTestHooks(t *testing.T) { + t.Helper() + + originalNewIDTokenSource := newIDTokenSource + originalMetadataGet := metadataGet + + t.Cleanup(func() { + newIDTokenSource = originalNewIDTokenSource + metadataGet = originalMetadataGet + }) +}