Skip to content

Commit

Permalink
feat(auth): add support for external accounts in detect (#8508)
Browse files Browse the repository at this point in the history
This is a direct follow up to #8491 and adds the last part of the detect package; external account support.
  • Loading branch information
codyoss committed Sep 6, 2023
1 parent a05b6ed commit 62210d5
Show file tree
Hide file tree
Showing 19 changed files with 4,700 additions and 2 deletions.
84 changes: 84 additions & 0 deletions auth/detect/detect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,90 @@ func TestDefaultCredentials_ClientCredentials(t *testing.T) {
}
}

// Better coverage of all external account features tested in the sub-package.
func TestDefaultCredentials_ExternalAccountKey(t *testing.T) {
b, err := os.ReadFile("../internal/testdata/exaccount_url.json")
if err != nil {
t.Fatal(err)
}
f, err := internaldetect.ParseExternalAccount(b)
if err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if r.URL.Path == "/token" {
resp := &struct {
Token string `json:"id_token"`
}{
Token: "a_fake_token_base",
}
if err := json.NewEncoder(w).Encode(&resp); err != nil {
t.Error(err)
}
} else if r.URL.Path == "/sts" {
r.ParseForm()
if got, want := r.Form.Get("subject_token"), "a_fake_token_base"; got != want {
t.Errorf("got %q, want %q", got, want)
}

resp := &struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
}{
AccessToken: "a_fake_token_sts",
ExpiresIn: 60,
}
if err := json.NewEncoder(w).Encode(&resp); err != nil {
t.Error(err)
}
} else if r.URL.Path == "/impersonate" {
if want := "a_fake_token_sts"; !strings.Contains(r.Header.Get("Authorization"), want) {
t.Errorf("missing sts token: got %q, want %q", r.Header.Get("Authorization"), want)
}

resp := &struct {
AccessToken string `json:"accessToken"`
ExpireTime string `json:"expireTime"`
}{
AccessToken: "a_fake_token",
ExpireTime: "2006-01-02T15:04:05Z",
}
if err := json.NewEncoder(w).Encode(&resp); err != nil {
t.Error(err)
}
} else {
t.Errorf("unexpected call to %q", r.URL.Path)
}
}))
f.ServiceAccountImpersonationURL = ts.URL + "/impersonate"
f.CredentialSource.URL = ts.URL + "/token"
f.TokenURL = ts.URL + "/sts"
b, err = json.Marshal(f)
if err != nil {
t.Fatal(err)
}

creds, err := DefaultCredentials(&Options{
CredentialsJSON: b,
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
UseSelfSignedJWT: true,
})
if err != nil {
t.Fatal(err)
}
tok, err := creds.Token(context.Background())
if err != nil {
t.Fatalf("creds.Token() = %v", err)
}
if want := "a_fake_token"; tok.Value != want {
t.Fatalf("got %q, want %q", tok.Value, want)
}
if want := internal.TokenTypeBearer; tok.Type != want {
t.Fatalf("got %q, want %q", tok.Type, want)
}
}

func TestDefaultCredentials_Fails(t *testing.T) {
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "nothingToSeeHere")
t.Setenv("HOME", "nothingToSeeHere")
Expand Down
30 changes: 30 additions & 0 deletions auth/detect/filetypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"fmt"

"cloud.google.com/go/auth"
"cloud.google.com/go/auth/detect/internal/externalaccount"
"cloud.google.com/go/auth/detect/internal/gdch"
"cloud.google.com/go/auth/detect/internal/impersonate"
"cloud.google.com/go/auth/internal/internaldetect"
Expand Down Expand Up @@ -53,6 +54,16 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
return nil, err
}
quotaProjectID = f.QuotaProjectID
case internaldetect.ExternalAccountKey:
f, err := internaldetect.ParseExternalAccount(b)
if err != nil {
return nil, err
}
tp, err = handleExternalAccount(f, opts)
if err != nil {
return nil, err
}
quotaProjectID = f.QuotaProjectID
case internaldetect.ImpersonatedServiceAccountKey:
f, err := internaldetect.ParseImpersonatedServiceAccount(b)
if err != nil {
Expand Down Expand Up @@ -111,6 +122,25 @@ func handleUserCredential(f *internaldetect.UserCredentialsFile, opts *Options)
return auth.New3LOTokenProvider(f.RefreshToken, opts3LO)
}

func handleExternalAccount(f *internaldetect.ExternalAccountFile, opts *Options) (auth.TokenProvider, error) {
externalOpts := &externalaccount.Options{
Audience: f.Audience,
SubjectTokenType: f.SubjectTokenType,
TokenURL: f.TokenURL,
TokenInfoURL: f.TokenInfoURL,
ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
ClientSecret: f.ClientSecret,
ClientID: f.ClientID,
CredentialSource: f.CredentialSource,
QuotaProjectID: f.QuotaProjectID,
Scopes: opts.scopes(),
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
Client: opts.client(),
}
return externalaccount.NewTokenProvider(externalOpts)
}

func handleImpersonatedServiceAccount(f *internaldetect.ImpersonatedServiceAccountFile, opts *Options) (auth.TokenProvider, error) {
if f.ServiceAccountImpersonationURL == "" || f.CredSource == nil {
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")
Expand Down

0 comments on commit 62210d5

Please sign in to comment.