Skip to content

Commit

Permalink
fix(auth): restore Application Default Credentials support to idtoken (
Browse files Browse the repository at this point in the history
  • Loading branch information
quartzmo authored Nov 6, 2024
1 parent 91c6f0f commit 8771f2e
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 55 deletions.
33 changes: 8 additions & 25 deletions auth/credentials/idtoken/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"strings"

"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/credentials/impersonate"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
Expand All @@ -32,14 +31,8 @@ const (
iamCredAud = "https://iamcredentials.googleapis.com/"
)

var (
defaultScopes = []string{
"https://iamcredentials.googleapis.com/",
"https://www.googleapis.com/auth/cloud-platform",
}
)

func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) {
func credsFromDefault(creds *auth.Credentials, opts *Options) (*auth.Credentials, error) {
b := creds.JSON()
t, err := credsfile.ParseFileType(b)
if err != nil {
return nil, err
Expand Down Expand Up @@ -93,33 +86,23 @@ func credsFromBytes(b []byte, opts *Options) (*auth.Credentials, error) {
account := filepath.Base(accountURL.ServiceAccountImpersonationURL)
account = strings.Split(account, ":")[0]

baseCreds, err := credentials.DetectDefault(&credentials.DetectOptions{
Scopes: defaultScopes,
CredentialsJSON: b,
Client: opts.client(),
UseSelfSignedJWT: true,
})
if err != nil {
return nil, err
}

config := impersonate.IDTokenOptions{
Audience: opts.Audience,
TargetPrincipal: account,
IncludeEmail: true,
Client: opts.client(),
Credentials: baseCreds,
Credentials: creds,
}
creds, err := impersonate.NewIDTokenCredentials(&config)
idTokenCreds, err := impersonate.NewIDTokenCredentials(&config)
if err != nil {
return nil, err
}
return auth.NewCredentials(&auth.CredentialsOptions{
TokenProvider: creds,
TokenProvider: idTokenCreds,
JSON: b,
ProjectIDProvider: auth.CredentialsPropertyFunc(baseCreds.ProjectID),
UniverseDomainProvider: auth.CredentialsPropertyFunc(baseCreds.UniverseDomain),
QuotaProjectIDProvider: auth.CredentialsPropertyFunc(baseCreds.QuotaProjectID),
ProjectIDProvider: auth.CredentialsPropertyFunc(creds.ProjectID),
UniverseDomainProvider: auth.CredentialsPropertyFunc(creds.UniverseDomain),
QuotaProjectIDProvider: auth.CredentialsPropertyFunc(creds.QuotaProjectID),
}), nil
default:
return nil, fmt.Errorf("idtoken: unsupported credentials type: %v", t)
Expand Down
24 changes: 18 additions & 6 deletions auth/credentials/idtoken/idtoken.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ package idtoken

import (
"errors"
"fmt"
"net/http"
"os"

"cloud.google.com/go/auth"
"cloud.google.com/go/auth/credentials"
"cloud.google.com/go/auth/internal"
"cloud.google.com/go/auth/internal/credsfile"
"cloud.google.com/go/compute/metadata"
Expand All @@ -52,6 +52,11 @@ const (
)

var (
defaultScopes = []string{
"https://iamcredentials.googleapis.com/",
"https://www.googleapis.com/auth/cloud-platform",
}

errMissingOpts = errors.New("idtoken: opts must be provided")
errMissingAudience = errors.New("idtoken: Audience must be provided")
errBothFileAndJSON = errors.New("idtoken: CredentialsFile and CredentialsJSON must not both be provided")
Expand Down Expand Up @@ -113,13 +118,20 @@ func NewCredentials(opts *Options) (*auth.Credentials, error) {
if err := opts.validate(); err != nil {
return nil, err
}
if b := opts.jsonBytes(); b != nil {
return credsFromBytes(b, opts)
}
if metadata.OnGCE() {
b := opts.jsonBytes()
if b == nil && metadata.OnGCE() {
return computeCredentials(opts)
}
return nil, fmt.Errorf("idtoken: couldn't find any credentials")
creds, err := credentials.DetectDefault(&credentials.DetectOptions{
Scopes: defaultScopes,
CredentialsJSON: b,
Client: opts.client(),
UseSelfSignedJWT: true,
})
if err != nil {
return nil, err
}
return credsFromDefault(creds, opts)
}

func (o *Options) jsonBytes() []byte {
Expand Down
75 changes: 51 additions & 24 deletions auth/credentials/idtoken/idtoken_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func TestNewCredentials_Validate(t *testing.T) {
}
}

func TestNewCredentials_ServiceAccount(t *testing.T) {
func TestNewCredentials_ServiceAccount_NoClient(t *testing.T) {
wantTok, _ := createRS256JWT(t)
b, err := os.ReadFile("../../internal/testdata/sa.json")
if err != nil {
Expand Down Expand Up @@ -116,30 +116,57 @@ func (m mockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
return rw.Result(), nil
}

func TestNewCredentials_ImpersonatedServiceAccount(t *testing.T) {
wantTok, _ := createRS256JWT(t)
client := internal.DefaultClient()
client.Transport = mockTransport{
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf(`{"token": %q}`, wantTok)))
}),
}
creds, err := NewCredentials(&Options{
Audience: "aud",
CredentialsFile: "../../internal/testdata/imp.json",
CustomClaims: map[string]interface{}{
"foo": "bar",
func TestNewCredentials_ImpersonatedAndExternal(t *testing.T) {
tests := []struct {
name string
adc string
file string
}{
{
name: "ADC external account",
adc: "../../internal/testdata/exaccount_url.json",
},
{
name: "CredentialsFile impersonated service account",
file: "../../internal/testdata/imp.json",
},
Client: client,
})
if err != nil {
t.Fatal(err)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("tp.Token() = %v", err)
}
if tok.Value != wantTok {
t.Errorf("got %q, want %q", tok.Value, wantTok)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wantTok, _ := createRS256JWT(t)
client := internal.DefaultClient()
client.Transport = mockTransport{
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(fmt.Sprintf(`{"token": %q}`, wantTok)))
}),
}

opts := &Options{
Audience: "aud",
CustomClaims: map[string]interface{}{
"foo": "bar",
},
Client: client,
}
if tt.file != "" {
opts.CredentialsFile = tt.file
} else if tt.adc != "" {
t.Setenv(credsfile.GoogleAppCredsEnvVar, tt.adc)
} else {
t.Fatal("test fixture must have adc or file")
}

creds, err := NewCredentials(opts)
if err != nil {
t.Fatal(err)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("tp.Token() = %v", err)
}
if tok.Value != wantTok {
t.Errorf("got %q, want %q", tok.Value, wantTok)
}
})
}
}

0 comments on commit 8771f2e

Please sign in to comment.