Skip to content

Commit 5823db5

Browse files
authored
feat(auth): port external account changes (#8697)
- golang/oauth2@18352fc - golang/oauth2@43b6a7b
1 parent 74b1547 commit 5823db5

25 files changed

+771
-87
lines changed

auth/detect/detect_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,64 @@ func TestDefaultCredentials_ExternalAccountKey(t *testing.T) {
538538
t.Fatalf("got %q, want %q", tok.Type, want)
539539
}
540540
}
541+
func TestDefaultCredentials_ExternalAccountAuthorizedUserKey(t *testing.T) {
542+
b, err := os.ReadFile("../internal/testdata/exaccount_user.json")
543+
if err != nil {
544+
t.Fatal(err)
545+
}
546+
f, err := internaldetect.ParseExternalAccountAuthorizedUser(b)
547+
if err != nil {
548+
t.Fatal(err)
549+
}
550+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
551+
defer r.Body.Close()
552+
if got, want := r.URL.Path, "/sts"; got != want {
553+
t.Errorf("got %q, want %q", got, want)
554+
}
555+
r.ParseForm()
556+
if got, want := r.Form.Get("refresh_token"), "refreshing"; got != want {
557+
t.Errorf("got %q, want %q", got, want)
558+
}
559+
if got, want := r.Form.Get("grant_type"), "refresh_token"; got != want {
560+
t.Errorf("got %q, want %q", got, want)
561+
}
562+
563+
resp := &struct {
564+
AccessToken string `json:"access_token"`
565+
ExpiresIn int `json:"expires_in"`
566+
}{
567+
AccessToken: "a_fake_token",
568+
ExpiresIn: 60,
569+
}
570+
if err := json.NewEncoder(w).Encode(&resp); err != nil {
571+
t.Error(err)
572+
}
573+
}))
574+
f.TokenURL = ts.URL + "/sts"
575+
b, err = json.Marshal(f)
576+
if err != nil {
577+
t.Fatal(err)
578+
}
579+
580+
creds, err := DefaultCredentials(&Options{
581+
CredentialsJSON: b,
582+
Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"},
583+
UseSelfSignedJWT: true,
584+
})
585+
if err != nil {
586+
t.Fatal(err)
587+
}
588+
tok, err := creds.Token(context.Background())
589+
if err != nil {
590+
t.Fatalf("creds.Token() = %v", err)
591+
}
592+
if want := "a_fake_token"; tok.Value != want {
593+
t.Fatalf("got %q, want %q", tok.Value, want)
594+
}
595+
if want := internal.TokenTypeBearer; tok.Type != want {
596+
t.Fatalf("got %q, want %q", tok.Type, want)
597+
}
598+
}
541599

542600
func TestDefaultCredentials_Fails(t *testing.T) {
543601
t.Setenv("GOOGLE_APPLICATION_CREDENTIALS", "nothingToSeeHere")

auth/detect/doc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@
6464
// executable-sourced credentials), please check out:
6565
// https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in
6666
//
67+
// # Security considerations
68+
//
6769
// Note that this library does not perform any validation on the token_url,
6870
// token_info_url, or service_account_impersonation_url fields of the credential
6971
// configuration. It is not recommended to use a credential configuration that

auth/detect/filetypes.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"cloud.google.com/go/auth"
2222
"cloud.google.com/go/auth/detect/internal/externalaccount"
23+
"cloud.google.com/go/auth/detect/internal/externalaccountuser"
2324
"cloud.google.com/go/auth/detect/internal/gdch"
2425
"cloud.google.com/go/auth/detect/internal/impersonate"
2526
"cloud.google.com/go/auth/internal/internaldetect"
@@ -66,6 +67,16 @@ func fileCredentials(b []byte, opts *Options) (*Credentials, error) {
6667
}
6768
quotaProjectID = f.QuotaProjectID
6869
universeDomain = f.UniverseDomain
70+
case internaldetect.ExternalAccountAuthorizedUserKey:
71+
f, err := internaldetect.ParseExternalAccountAuthorizedUser(b)
72+
if err != nil {
73+
return nil, err
74+
}
75+
tp, err = handleExternalAccountAuthorizedUser(f, opts)
76+
if err != nil {
77+
return nil, err
78+
}
79+
quotaProjectID = f.QuotaProjectID
6980
case internaldetect.ImpersonatedServiceAccountKey:
7081
f, err := internaldetect.ParseImpersonatedServiceAccount(b)
7182
if err != nil {
@@ -145,6 +156,20 @@ func handleExternalAccount(f *internaldetect.ExternalAccountFile, opts *Options)
145156
return externalaccount.NewTokenProvider(externalOpts)
146157
}
147158

159+
func handleExternalAccountAuthorizedUser(f *internaldetect.ExternalAccountAuthorizedUserFile, opts *Options) (auth.TokenProvider, error) {
160+
externalOpts := &externalaccountuser.Options{
161+
Audience: f.Audience,
162+
RefreshToken: f.RefreshToken,
163+
TokenURL: f.TokenURL,
164+
TokenInfoURL: f.TokenInfoURL,
165+
ClientID: f.ClientID,
166+
ClientSecret: f.ClientSecret,
167+
Scopes: opts.scopes(),
168+
Client: opts.client(),
169+
}
170+
return externalaccountuser.NewTokenProvider(externalOpts)
171+
}
172+
148173
func handleImpersonatedServiceAccount(f *internaldetect.ImpersonatedServiceAccountFile, opts *Options) (auth.TokenProvider, error) {
149174
if f.ServiceAccountImpersonationURL == "" || f.CredSource == nil {
150175
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")

auth/detect/internal/externalaccount/aws_provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const (
6969

7070
awsTimeFormatLong = "20060102T150405Z"
7171
awsTimeFormatShort = "20060102"
72+
awsProviderType = "aws"
7273
)
7374

7475
type awsSubjectProvider struct {
@@ -168,6 +169,10 @@ func (sp *awsSubjectProvider) subjectToken(ctx context.Context) (string, error)
168169
return url.QueryEscape(string(result)), nil
169170
}
170171

172+
func (sp *awsSubjectProvider) providerType() string {
173+
return awsProviderType
174+
}
175+
171176
func (cs *awsSubjectProvider) getAWSSessionToken(ctx context.Context) (string, error) {
172177
if cs.IMDSv2SessionTokenURL == "" {
173178
return "", nil

auth/detect/internal/externalaccount/aws_provider_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,10 @@ func TestAWSCredential_BasicRequest(t *testing.T) {
574574
t.Fatalf("retrieveSubjectToken() failed: %v", err)
575575
}
576576

577+
if got, want := base.providerType(), awsProviderType; got != want {
578+
t.Fatalf("got %q, want %q", got, want)
579+
}
580+
577581
want := getExpectedSubjectToken(
578582
"https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
579583
"us-east-2",

auth/detect/internal/externalaccount/executable_provider.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const (
3434
executableSupportedMaxVersion = 1
3535
executableDefaultTimeout = 30 * time.Second
3636
executableSource = "response"
37+
executableProviderType = "executable"
3738
outputFileSource = "output file"
3839

3940
allowExecutablesEnvVar = "GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES"
@@ -175,6 +176,10 @@ func (cs *executableSubjectProvider) subjectToken(ctx context.Context) (string,
175176
return cs.getTokenFromExecutableCommand(ctx)
176177
}
177178

179+
func (cs *executableSubjectProvider) providerType() string {
180+
return executableProviderType
181+
}
182+
178183
func (cs *executableSubjectProvider) getTokenFromOutputFile() (token string, err error) {
179184
if cs.OutputFile == "" {
180185
// This ExecutableCredentialSource doesn't use an OutputFile.

auth/detect/internal/externalaccount/executable_provider_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,9 @@ func TestRetrieveExecutableSubjectTokenExecutableErrors(t *testing.T) {
445445
t.Run(tt.name, func(t *testing.T) {
446446
ecs.env = &tt.testEnvironment
447447

448+
if got, want := ecs.providerType(), executableProviderType; got != want {
449+
t.Fatalf("got %q, want %q", got, want)
450+
}
448451
if _, err = ecs.subjectToken(context.Background()); err == nil {
449452
t.Fatalf("got nil, want an error")
450453
} else if tt.skipErrorEquals {

auth/detect/internal/externalaccount/externalaccount.go

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,11 @@ import (
2525

2626
"cloud.google.com/go/auth"
2727
"cloud.google.com/go/auth/detect/internal/impersonate"
28+
"cloud.google.com/go/auth/detect/internal/stsexchange"
2829
"cloud.google.com/go/auth/internal/internaldetect"
2930
)
3031

3132
const (
32-
stsGrantType = "urn:ietf:params:oauth:grant-type:token-exchange"
33-
stsTokenType = "urn:ietf:params:oauth:token-type:access_token"
34-
3533
timeoutMinimum = 5 * time.Second
3634
timeoutMaximum = 120 * time.Second
3735
)
@@ -127,6 +125,7 @@ func NewTokenProvider(opts *Options) (auth.TokenProvider, error) {
127125

128126
type subjectTokenProvider interface {
129127
subjectToken(ctx context.Context) (string, error)
128+
providerType() string
130129
}
131130

132131
// tokenProvider is the provider that handles external credentials. It is used to retrieve Tokens.
@@ -142,17 +141,18 @@ func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
142141
return nil, err
143142
}
144143

145-
stsRequest := &stsTokenExchangeRequest{
146-
GrantType: stsGrantType,
144+
stsRequest := &stsexchange.TokenRequest{
145+
GrantType: stsexchange.GrantType,
147146
Audience: tp.opts.Audience,
148147
Scope: tp.opts.Scopes,
149-
RequestedTokenType: stsTokenType,
148+
RequestedTokenType: stsexchange.TokenType,
150149
SubjectToken: subjectToken,
151150
SubjectTokenType: tp.opts.SubjectTokenType,
152151
}
153152
header := make(http.Header)
154153
header.Set("Content-Type", "application/x-www-form-urlencoded")
155-
clientAuth := clientAuthentication{
154+
header.Add("x-goog-api-client", getGoogHeaderValue(tp.opts, tp.stp))
155+
clientAuth := stsexchange.ClientAuthentication{
156156
AuthStyle: auth.StyleInHeader,
157157
ClientID: tp.opts.ClientID,
158158
ClientSecret: tp.opts.ClientSecret,
@@ -165,13 +165,13 @@ func (tp *tokenProvider) Token(ctx context.Context) (*auth.Token, error) {
165165
"userProject": tp.opts.WorkforcePoolUserProject,
166166
}
167167
}
168-
stsResp, err := exchangeToken(ctx, &exchangeOptions{
169-
client: tp.client,
170-
endpoint: tp.opts.TokenURL,
171-
request: stsRequest,
172-
authentication: clientAuth,
173-
headers: header,
174-
extraOpts: options,
168+
stsResp, err := stsexchange.ExchangeToken(ctx, &stsexchange.Options{
169+
Client: tp.client,
170+
Endpoint: tp.opts.TokenURL,
171+
Request: stsRequest,
172+
Authentication: clientAuth,
173+
Headers: header,
174+
ExtraOpts: options,
175175
})
176176
if err != nil {
177177
return nil, err
@@ -240,3 +240,12 @@ func newSubjectTokenProvider(o *Options) (subjectTokenProvider, error) {
240240
}
241241
return nil, errors.New("detect: unable to parse credential source")
242242
}
243+
244+
func getGoogHeaderValue(conf *Options, p subjectTokenProvider) string {
245+
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
246+
goVersion(),
247+
"unknown",
248+
p.providerType(),
249+
conf.ServiceAccountImpersonationURL != "",
250+
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
251+
}

auth/detect/internal/externalaccount/externalaccount_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package externalaccount
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"io"
2021
"net/http"
2122
"net/http/httptest"
@@ -72,6 +73,7 @@ func TestToken(t *testing.T) {
7273
contentType: "application/x-www-form-urlencoded",
7374
body: baseCredsRequestBody,
7475
response: baseCredsResponseBody,
76+
metricsHeader: expectedMetricsHeader("file", false, false),
7577
}
7678

7779
tok, err := run(t, opts, server)
@@ -98,6 +100,7 @@ func TestWorkforcePoolTokenWithClientID(t *testing.T) {
98100
contentType: "application/x-www-form-urlencoded",
99101
body: workforcePoolRequestBodyWithClientID,
100102
response: baseCredsResponseBody,
103+
metricsHeader: expectedMetricsHeader("file", false, false),
101104
}
102105

103106
tok, err := run(t, &opts, &server)
@@ -123,6 +126,7 @@ func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
123126
contentType: "application/x-www-form-urlencoded",
124127
body: workforcePoolRequestBodyWithoutClientID,
125128
response: baseCredsResponseBody,
129+
metricsHeader: expectedMetricsHeader("file", false, false),
126130
}
127131

128132
tok, err := run(t, &opts, &server)
@@ -196,6 +200,7 @@ type testExchangeTokenServer struct {
196200
contentType string
197201
body string
198202
response string
203+
metricsHeader string
199204
}
200205

201206
func run(t *testing.T, opts *Options, tets *testExchangeTokenServer) (*auth.Token, error) {
@@ -211,6 +216,10 @@ func run(t *testing.T, opts *Options, tets *testExchangeTokenServer) (*auth.Toke
211216
if got, want := headerContentType, tets.contentType; got != want {
212217
t.Errorf("got %v, want %v", got, want)
213218
}
219+
headerMetrics := r.Header.Get("x-goog-api-client")
220+
if got, want := headerMetrics, tets.metricsHeader; got != want {
221+
t.Errorf("got %v but want %v", got, want)
222+
}
214223
body, err := io.ReadAll(r.Body)
215224
if err != nil {
216225
t.Fatalf("Failed reading request body: %s.", err)
@@ -266,3 +275,7 @@ func cloneTestOpts() *Options {
266275
Client: internal.CloneDefaultClient(),
267276
}
268277
}
278+
279+
func expectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string {
280+
return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime)
281+
}

auth/detect/internal/externalaccount/file_provider.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import (
2626
"cloud.google.com/go/auth/internal/internaldetect"
2727
)
2828

29+
const (
30+
fileProviderType = "file"
31+
)
32+
2933
type fileSubjectProvider struct {
3034
File string
3135
Format internaldetect.Format
@@ -64,3 +68,7 @@ func (sp *fileSubjectProvider) subjectToken(context.Context) (string, error) {
6468
return "", errors.New("detect: invalid credential_source file format type: " + sp.Format.Type)
6569
}
6670
}
71+
72+
func (sp *fileSubjectProvider) providerType() string {
73+
return fileProviderType
74+
}

auth/detect/internal/externalaccount/file_provider_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
6767
} else if test.want != out {
6868
t.Errorf("got %v, want %v", out, test.want)
6969
}
70-
70+
if got, want := base.providerType(), fileProviderType; got != want {
71+
t.Fatalf("got %q, want %q", got, want)
72+
}
7173
})
7274
}
7375
}

auth/detect/internal/externalaccount/impersonate_test.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ var (
3131

3232
func TestImpersonation(t *testing.T) {
3333
var impersonationTests = []struct {
34-
name string
35-
opts *Options
36-
wantBody string
34+
name string
35+
opts *Options
36+
wantBody string
37+
metricsHeader string
3738
}{
3839
{
3940
name: "Base Impersonation",
@@ -46,7 +47,8 @@ func TestImpersonation(t *testing.T) {
4647
CredentialSource: testBaseCredSource,
4748
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
4849
},
49-
wantBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
50+
wantBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
51+
metricsHeader: expectedMetricsHeader("file", true, false),
5052
},
5153
{
5254
name: "With TokenLifetime Set",
@@ -60,7 +62,8 @@ func TestImpersonation(t *testing.T) {
6062
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
6163
ServiceAccountImpersonationLifetimeSeconds: 10000,
6264
},
63-
wantBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
65+
wantBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
66+
metricsHeader: expectedMetricsHeader("file", true, false),
6467
},
6568
}
6669
for _, tt := range impersonationTests {

0 commit comments

Comments
 (0)