From f9869f7903cfd34d1b97c25d0dc5669d2c5138e6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 10 Sep 2024 12:01:06 -0600 Subject: [PATCH] fix(auth): enable self-signed JWT for non-GDU universe domain (#10831) * default to UseSelfSignedJWT=true for non-GDU service account flows * add error for UseSelfSignedJWT without aud or scope --- auth/credentials/detect_test.go | 42 ++++++++++-------- auth/credentials/filetypes.go | 6 +++ auth/credentials/selfsignedjwt.go | 4 ++ auth/credentials/selfsignedjwt_test.go | 44 +++++++++++++++++-- .../testdata/imp_universe_domain.json | 2 +- 5 files changed, 75 insertions(+), 23 deletions(-) diff --git a/auth/credentials/detect_test.go b/auth/credentials/detect_test.go index 4e8672159b66..882ba565cc6c 100644 --- a/auth/credentials/detect_test.go +++ b/auth/credentials/detect_test.go @@ -459,11 +459,10 @@ func TestDefaultCredentials_ServiceAccountKeySelfSigned_UniverseDomain(t *testin now = func() time.Time { return time.Date(2000, 2, 1, 12, 30, 0, 0, time.UTC) } defer func() { now = oldNow }() wantTok := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiY2RlZjEyMzQ1Njc4OTAifQ.eyJpc3MiOiJnb3BoZXJAZmFrZV9wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwic2NvcGUiOiJodHRwczovL3d3dy5nb29nbGVhcGlzLmNvbS9hdXRoL2Nsb3VkLXBsYXRmb3JtIiwiZXhwIjo5NDk0MTE4MDAsImlhdCI6OTQ5NDA4MjAwLCJhdWQiOiIiLCJzdWIiOiJnb3BoZXJAZmFrZV9wcm9qZWN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIn0.n9Hggd-1Vw4WTQiWkh7q9r5eDsz-khU5vwkZl2VmgdUF3ZxDq1ARzchCNtTifeorzbp9C0i0vCr855G7FZkVCJXPVMcnxbwfMSafUYmVsmutbQiV9eTWfWM0_Ljiwa9GEbv1bN06Lz4LrelPKEaxsDbY6tU8LJUiome_gSMLfLk" - creds, err := DetectDefault(&DetectOptions{ - CredentialsJSON: b, - Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, - UseSelfSignedJWT: true, + // default scopes are set in resolveDetectOptions before calling DetectDefault. + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + CredentialsJSON: b, }) if err != nil { t.Fatal(err) @@ -805,26 +804,29 @@ func TestDefaultCredentials_UniverseDomain(t *testing.T) { { name: "service account json with file universe domain", opts: &DetectOptions{ - CredentialsFile: "../internal/testdata/sa_universe_domain.json", - UseSelfSignedJWT: true, + CredentialsFile: "../internal/testdata/sa_universe_domain.json", + // default scopes are set in resolveDetectOptions before calling DetectDefault. + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, }, want: "example.com", }, { name: "service account json with options universe domain", opts: &DetectOptions{ - CredentialsFile: "../internal/testdata/sa.json", - UseSelfSignedJWT: true, - UniverseDomain: "foo.com", + CredentialsFile: "../internal/testdata/sa.json", + // default scopes are set in resolveDetectOptions before calling DetectDefault. + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + UniverseDomain: "foo.com", }, want: "foo.com", }, { name: "service account json with file and options universe domain", opts: &DetectOptions{ - CredentialsFile: "../internal/testdata/sa_universe_domain.json", - UseSelfSignedJWT: true, - UniverseDomain: "foo.com", + CredentialsFile: "../internal/testdata/sa_universe_domain.json", + // default scopes are set in resolveDetectOptions before calling DetectDefault. + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + UniverseDomain: "foo.com", }, want: "foo.com", }, @@ -923,8 +925,9 @@ func TestDefaultCredentials_UniverseDomain(t *testing.T) { { name: "impersonated service account json", opts: &DetectOptions{ - CredentialsFile: "../internal/testdata/imp.json", - UseSelfSignedJWT: true, + CredentialsFile: "../internal/testdata/imp.json", + // default scopes are set in resolveDetectOptions before calling DetectDefault. + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, }, want: "googleapis.com", }, @@ -938,9 +941,10 @@ func TestDefaultCredentials_UniverseDomain(t *testing.T) { { name: "impersonated service account json with options universe domain", opts: &DetectOptions{ - CredentialsFile: "../internal/testdata/imp.json", - UseSelfSignedJWT: true, - UniverseDomain: "foo.com", + CredentialsFile: "../internal/testdata/imp.json", + // default scopes are set in resolveDetectOptions before calling DetectDefault. + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + UniverseDomain: "foo.com", }, want: "foo.com", }, @@ -948,7 +952,9 @@ func TestDefaultCredentials_UniverseDomain(t *testing.T) { name: "impersonated service account json with file and options universe domain", opts: &DetectOptions{ CredentialsFile: "../internal/testdata/imp_universe_domain.json", - UniverseDomain: "foo.com", + // default scopes are set in resolveDetectOptions before calling DetectDefault. + Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}, + UniverseDomain: "foo.com", }, want: "foo.com", }, diff --git a/auth/credentials/filetypes.go b/auth/credentials/filetypes.go index cf56b025a237..6591b181132f 100644 --- a/auth/credentials/filetypes.go +++ b/auth/credentials/filetypes.go @@ -124,8 +124,14 @@ func resolveUniverseDomain(optsUniverseDomain, fileUniverseDomain string) string } func handleServiceAccount(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) { + ud := resolveUniverseDomain(opts.UniverseDomain, f.UniverseDomain) if opts.UseSelfSignedJWT { return configureSelfSignedJWT(f, opts) + } else if ud != "" && ud != internalauth.DefaultUniverseDomain { + // For non-GDU universe domains, token exchange is impossible and services + // must support self-signed JWTs. + opts.UseSelfSignedJWT = true + return configureSelfSignedJWT(f, opts) } opts2LO := &auth.Options2LO{ Email: f.ClientEmail, diff --git a/auth/credentials/selfsignedjwt.go b/auth/credentials/selfsignedjwt.go index b62a8ae4d5d7..6ae29de6c278 100644 --- a/auth/credentials/selfsignedjwt.go +++ b/auth/credentials/selfsignedjwt.go @@ -17,6 +17,7 @@ package credentials import ( "context" "crypto/rsa" + "errors" "fmt" "strings" "time" @@ -35,6 +36,9 @@ var ( // configureSelfSignedJWT uses the private key in the service account to create // a JWT without making a network call. func configureSelfSignedJWT(f *credsfile.ServiceAccountFile, opts *DetectOptions) (auth.TokenProvider, error) { + if len(opts.scopes()) == 0 && opts.Audience == "" { + return nil, errors.New("credentials: both scopes and audience are empty") + } pk, err := internal.ParseKey([]byte(f.PrivateKey)) if err != nil { return nil, fmt.Errorf("credentials: could not parse key: %w", err) diff --git a/auth/credentials/selfsignedjwt_test.go b/auth/credentials/selfsignedjwt_test.go index daa6b90c6629..19fc79f78071 100644 --- a/auth/credentials/selfsignedjwt_test.go +++ b/auth/credentials/selfsignedjwt_test.go @@ -40,7 +40,7 @@ var jwtJSONKey = []byte(`{ "audience": "https://testpervice.googleapis.com/" }`) -func TestDefaultCredentials_SelfSignedJSON(t *testing.T) { +func TestDetectDefault_SelfSignedJSON(t *testing.T) { privateKey, jsonKey, err := setupFakeKey() if err != nil { t.Fatal(err) @@ -51,7 +51,7 @@ func TestDefaultCredentials_SelfSignedJSON(t *testing.T) { UseSelfSignedJWT: true, }) if err != nil { - t.Fatalf("DefaultCredentials(%s): %v", jsonKey, err) + t.Fatalf("DetectDefault(%s): %v", jsonKey, err) } tok, err := tp.Token(context.Background()) @@ -102,7 +102,7 @@ func TestDefaultCredentials_SelfSignedJSON(t *testing.T) { } } -func TestDefaultCredentials_SelfSignedWithScope(t *testing.T) { +func TestDetectDefault_SelfSignedWithScope(t *testing.T) { privateKey, jsonKey, err := setupFakeKey() if err != nil { t.Fatal(err) @@ -113,7 +113,7 @@ func TestDefaultCredentials_SelfSignedWithScope(t *testing.T) { UseSelfSignedJWT: true, }) if err != nil { - t.Fatalf("DefaultCredentials(%s): %v", jsonKey, err) + t.Fatalf("DetectDefault(%s): %v", jsonKey, err) } tok, err := tp.Token(context.Background()) @@ -164,6 +164,42 @@ func TestDefaultCredentials_SelfSignedWithScope(t *testing.T) { } } +func TestDetectDefault_SelfSignedWithAudienceAndScope(t *testing.T) { + _, jsonKey, err := setupFakeKey() + if err != nil { + t.Fatal(err) + } + _, err = DetectDefault(&DetectOptions{ + CredentialsJSON: jsonKey, + Audience: "audience", + Scopes: []string{"scope1", "scope2"}, + UseSelfSignedJWT: true, + }) + if err == nil { + t.Fatal("DetectDefault(): want non-nil err") + } + if want := "credentials: both scopes and audience were provided"; err.Error() != want { + t.Errorf("TokenType = %q, want %q", err, want) + } +} + +func TestDetectDefault_SelfSignedWithoutAudienceOrScope(t *testing.T) { + _, jsonKey, err := setupFakeKey() + if err != nil { + t.Fatal(err) + } + _, err = DetectDefault(&DetectOptions{ + CredentialsJSON: jsonKey, + UseSelfSignedJWT: true, + }) + if err == nil { + t.Fatal("DetectDefault(): want non-nil err") + } + if want := "credentials: both scopes and audience are empty"; err.Error() != want { + t.Errorf("DetectDefault = %q, want %q", err, want) + } +} + // setupFakeKey generates a key we can use in the test data. func setupFakeKey() (*rsa.PrivateKey, []byte, error) { // Generate a key we can use in the test data. diff --git a/auth/internal/testdata/imp_universe_domain.json b/auth/internal/testdata/imp_universe_domain.json index 36720fa5b210..09f0c06ea858 100644 --- a/auth/internal/testdata/imp_universe_domain.json +++ b/auth/internal/testdata/imp_universe_domain.json @@ -8,7 +8,7 @@ "type": "service_account", "project_id": "fake_project", "private_key_id": "89asd789789uo473454c47543", - "private_key": "fake", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12ikv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/GrCtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrPSXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAutLPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEAgidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ==\n-----END PRIVATE KEY-----\n", "client_email": "sa@fake_project.iam.gserviceaccount.com", "client_id": "gopher", "auth_uri": "https://accounts.google.com/o/oauth2/auth",