Skip to content

Commit

Permalink
fix(auth): enable self-signed JWT for non-GDU universe domain (#10831)
Browse files Browse the repository at this point in the history
* default to UseSelfSignedJWT=true for non-GDU service account flows
* add error for UseSelfSignedJWT without aud or scope
  • Loading branch information
quartzmo authored Sep 10, 2024
1 parent 6720291 commit f9869f7
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 23 deletions.
42 changes: 24 additions & 18 deletions auth/credentials/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand All @@ -938,17 +941,20 @@ 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",
},
{
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",
},
Expand Down
6 changes: 6 additions & 0 deletions auth/credentials/filetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions auth/credentials/selfsignedjwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package credentials
import (
"context"
"crypto/rsa"
"errors"
"fmt"
"strings"
"time"
Expand All @@ -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)
Expand Down
44 changes: 40 additions & 4 deletions auth/credentials/selfsignedjwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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())
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion auth/internal/testdata/imp_universe_domain.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit f9869f7

Please sign in to comment.