Skip to content

Commit 62210d5

Browse files
authored
feat(auth): add support for external accounts in detect (#8508)
This is a direct follow up to #8491 and adds the last part of the detect package; external account support.
1 parent a05b6ed commit 62210d5

19 files changed

Lines changed: 4700 additions & 2 deletions

auth/detect/detect_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,90 @@ func TestDefaultCredentials_ClientCredentials(t *testing.T) {
359359
}
360360
}
361361

362+
// Better coverage of all external account features tested in the sub-package.
363+
func TestDefaultCredentials_ExternalAccountKey(t *testing.T) {
364+
b, err := os.ReadFile("../internal/testdata/exaccount_url.json")
365+
if err != nil {
366+
t.Fatal(err)
367+
}
368+
f, err := internaldetect.ParseExternalAccount(b)
369+
if err != nil {
370+
t.Fatal(err)
371+
}
372+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
373+
defer r.Body.Close()
374+
if r.URL.Path == "/token" {
375+
resp := &struct {
376+
Token string `json:"id_token"`
377+
}{
378+
Token: "a_fake_token_base",
379+
}
380+
if err := json.NewEncoder(w).Encode(&resp); err != nil {
381+
t.Error(err)
382+
}
383+
} else if r.URL.Path == "/sts" {
384+
r.ParseForm()
385+
if got, want := r.Form.Get("subject_token"), "a_fake_token_base"; got != want {
386+
t.Errorf("got %q, want %q", got, want)
387+
}
388+
389+
resp := &struct {
390+
AccessToken string `json:"access_token"`
391+
ExpiresIn int `json:"expires_in"`
392+
}{
393+
AccessToken: "a_fake_token_sts",
394+
ExpiresIn: 60,
395+
}
396+
if err := json.NewEncoder(w).Encode(&resp); err != nil {
397+
t.Error(err)
398+
}
399+
} else if r.URL.Path == "/impersonate" {
400+
if want := "a_fake_token_sts"; !strings.Contains(r.Header.Get("Authorization"), want) {
401+
t.Errorf("missing sts token: got %q, want %q", r.Header.Get("Authorization"), want)
402+
}
403+
404+
resp := &struct {
405+
AccessToken string `json:"accessToken"`
406+
ExpireTime string `json:"expireTime"`
407+
}{
408+
AccessToken: "a_fake_token",
409+
ExpireTime: "2006-01-02T15:04:05Z",
410+
}
411+
if err := json.NewEncoder(w).Encode(&resp); err != nil {
412+
t.Error(err)
413+
}
414+
} else {
415+
t.Errorf("unexpected call to %q", r.URL.Path)
416+
}
417+
}))
418+
f.ServiceAccountImpersonationURL = ts.URL + "/impersonate"
419+
f.CredentialSource.URL = ts.URL + "/token"
420+
f.TokenURL = ts.URL + "/sts"
421+
b, err = json.Marshal(f)
422+
if err != nil {
423+
t.Fatal(err)
424+
}
425+
426+
creds, err := DefaultCredentials(&Options{
427+
CredentialsJSON: b,
428+
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
429+
UseSelfSignedJWT: true,
430+
})
431+
if err != nil {
432+
t.Fatal(err)
433+
}
434+
tok, err := creds.Token(context.Background())
435+
if err != nil {
436+
t.Fatalf("creds.Token() = %v", err)
437+
}
438+
if want := "a_fake_token"; tok.Value != want {
439+
t.Fatalf("got %q, want %q", tok.Value, want)
440+
}
441+
if want := internal.TokenTypeBearer; tok.Type != want {
442+
t.Fatalf("got %q, want %q", tok.Type, want)
443+
}
444+
}
445+
362446
func TestDefaultCredentials_Fails(t *testing.T) {
363447
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "nothingToSeeHere")
364448
t.Setenv("HOME", "nothingToSeeHere")

auth/detect/filetypes.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020

2121
"cloud.google.com/go/auth"
22+
"cloud.google.com/go/auth/detect/internal/externalaccount"
2223
"cloud.google.com/go/auth/detect/internal/gdch"
2324
"cloud.google.com/go/auth/detect/internal/impersonate"
2425
"cloud.google.com/go/auth/internal/internaldetect"
@@ -53,6 +54,16 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
5354
return nil, err
5455
}
5556
quotaProjectID = f.QuotaProjectID
57+
case internaldetect.ExternalAccountKey:
58+
f, err := internaldetect.ParseExternalAccount(b)
59+
if err != nil {
60+
return nil, err
61+
}
62+
tp, err = handleExternalAccount(f, opts)
63+
if err != nil {
64+
return nil, err
65+
}
66+
quotaProjectID = f.QuotaProjectID
5667
case internaldetect.ImpersonatedServiceAccountKey:
5768
f, err := internaldetect.ParseImpersonatedServiceAccount(b)
5869
if err != nil {
@@ -111,6 +122,25 @@ func handleUserCredential(f *internaldetect.UserCredentialsFile, opts *Options)
111122
return auth.New3LOTokenProvider(f.RefreshToken, opts3LO)
112123
}
113124

125+
func handleExternalAccount(f *internaldetect.ExternalAccountFile, opts *Options) (auth.TokenProvider, error) {
126+
externalOpts := &externalaccount.Options{
127+
Audience: f.Audience,
128+
SubjectTokenType: f.SubjectTokenType,
129+
TokenURL: f.TokenURL,
130+
TokenInfoURL: f.TokenInfoURL,
131+
ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
132+
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
133+
ClientSecret: f.ClientSecret,
134+
ClientID: f.ClientID,
135+
CredentialSource: f.CredentialSource,
136+
QuotaProjectID: f.QuotaProjectID,
137+
Scopes: opts.scopes(),
138+
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
139+
Client: opts.client(),
140+
}
141+
return externalaccount.NewTokenProvider(externalOpts)
142+
}
143+
114144
func handleImpersonatedServiceAccount(f *internaldetect.ImpersonatedServiceAccountFile, opts *Options) (auth.TokenProvider, error) {
115145
if f.ServiceAccountImpersonationURL == "" || f.CredSource == nil {
116146
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")

0 commit comments

Comments
 (0)