Skip to content

Commit c8730f7

Browse files
ScruffyProdigycodyoss
authored andcommitted
google/internal/externalaccount: allow impersonation lifetime changes
Right now, impersonation tokens used for external accounts have a hardcoded lifetime of 1 hour (3600 seconds), but some of our customers want to be able to adjust this lifetime. These changes (along with others in the gcloud cli) should allow this Change-Id: I705f83dc2a092d8cdd0fcbfff83b014c220e28bb GitHub-Last-Rev: 7e0ea92 GitHub-Pull-Request: golang#571 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/416797 Reviewed-by: Cody Oss <[email protected]> Reviewed-by: Shin Fan <[email protected]> Run-TryBot: Cody Oss <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 2104d58 commit c8730f7

File tree

4 files changed

+104
-48
lines changed

4 files changed

+104
-48
lines changed

google/google.go

+12-6
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ type credentialsFile struct {
122122
TokenURLExternal string `json:"token_url"`
123123
TokenInfoURL string `json:"token_info_url"`
124124
ServiceAccountImpersonationURL string `json:"service_account_impersonation_url"`
125+
ServiceAccountImpersonation serviceAccountImpersonationInfo `json:"service_account_impersonation"`
125126
Delegates []string `json:"delegates"`
126127
CredentialSource externalaccount.CredentialSource `json:"credential_source"`
127128
QuotaProjectID string `json:"quota_project_id"`
@@ -131,6 +132,10 @@ type credentialsFile struct {
131132
SourceCredentials *credentialsFile `json:"source_credentials"`
132133
}
133134

135+
type serviceAccountImpersonationInfo struct {
136+
TokenLifetimeSeconds int `json:"token_lifetime_seconds"`
137+
}
138+
134139
func (f *credentialsFile) jwtConfig(scopes []string, subject string) *jwt.Config {
135140
cfg := &jwt.Config{
136141
Email: f.ClientEmail,
@@ -178,12 +183,13 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
178183
TokenURL: f.TokenURLExternal,
179184
TokenInfoURL: f.TokenInfoURL,
180185
ServiceAccountImpersonationURL: f.ServiceAccountImpersonationURL,
181-
ClientSecret: f.ClientSecret,
182-
ClientID: f.ClientID,
183-
CredentialSource: f.CredentialSource,
184-
QuotaProjectID: f.QuotaProjectID,
185-
Scopes: params.Scopes,
186-
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
186+
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
187+
ClientSecret: f.ClientSecret,
188+
ClientID: f.ClientID,
189+
CredentialSource: f.CredentialSource,
190+
QuotaProjectID: f.QuotaProjectID,
191+
Scopes: params.Scopes,
192+
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
187193
}
188194
return cfg.TokenSource(ctx)
189195
case impersonatedServiceAccount:

google/internal/externalaccount/basecredentials.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ type Config struct {
3939
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
4040
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
4141
ServiceAccountImpersonationURL string
42+
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
43+
// token will be valid for.
44+
ServiceAccountImpersonationLifetimeSeconds int
4245
// ClientSecret is currently only required if token_info endpoint also
4346
// needs to be called with the generated GCP access token. When provided, STS will be
4447
// called with additional basic authentication using client_id as username and client_secret as password.
@@ -141,10 +144,11 @@ func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Re
141144
scopes := c.Scopes
142145
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
143146
imp := ImpersonateTokenSource{
144-
Ctx: ctx,
145-
URL: c.ServiceAccountImpersonationURL,
146-
Scopes: scopes,
147-
Ts: oauth2.ReuseTokenSource(nil, ts),
147+
Ctx: ctx,
148+
URL: c.ServiceAccountImpersonationURL,
149+
Scopes: scopes,
150+
Ts: oauth2.ReuseTokenSource(nil, ts),
151+
TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
148152
}
149153
return oauth2.ReuseTokenSource(nil, imp), nil
150154
}

google/internal/externalaccount/impersonate.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,19 @@ type ImpersonateTokenSource struct {
4848
// Each service account must be granted roles/iam.serviceAccountTokenCreator
4949
// on the next service account in the chain. Optional.
5050
Delegates []string
51+
// TokenLifetimeSeconds is the number of seconds the impersonation token will
52+
// be valid for.
53+
TokenLifetimeSeconds int
5154
}
5255

5356
// Token performs the exchange to get a temporary service account token to allow access to GCP.
5457
func (its ImpersonateTokenSource) Token() (*oauth2.Token, error) {
58+
lifetimeString := "3600s"
59+
if its.TokenLifetimeSeconds != 0 {
60+
lifetimeString = fmt.Sprintf("%ds", its.TokenLifetimeSeconds)
61+
}
5562
reqBody := generateAccessTokenReq{
56-
Lifetime: "3600s",
63+
Lifetime: lifetimeString,
5764
Scope: its.Scopes,
5865
Delegates: its.Delegates,
5966
}

google/internal/externalaccount/impersonate_test.go

+76-37
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,18 @@ import (
1313
"testing"
1414
)
1515

16-
var testImpersonateConfig = Config{
17-
Audience: "32555940559.apps.googleusercontent.com",
18-
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
19-
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
20-
ClientSecret: "notsosecret",
21-
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
22-
CredentialSource: testBaseCredSource,
23-
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
24-
}
25-
2616
var (
2717
baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
2818
baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}`
2919
)
3020

31-
func TestImpersonation(t *testing.T) {
32-
impersonateServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33-
if got, want := r.URL.String(), "/"; got != want {
21+
func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server {
22+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23+
if got, want := r.URL.String(), urlWanted; got != want {
3424
t.Errorf("URL.String(): got %v but want %v", got, want)
3525
}
3626
headerAuth := r.Header.Get("Authorization")
37-
if got, want := headerAuth, "Bearer Sample.Access.Token"; got != want {
27+
if got, want := headerAuth, authWanted; got != want {
3828
t.Errorf("got %v but want %v", got, want)
3929
}
4030
headerContentType := r.Header.Get("Content-Type")
@@ -45,14 +35,16 @@ func TestImpersonation(t *testing.T) {
4535
if err != nil {
4636
t.Fatalf("Failed reading request body: %v.", err)
4737
}
48-
if got, want := string(body), "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}"; got != want {
38+
if got, want := string(body), bodyWanted; got != want {
4939
t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want)
5040
}
5141
w.Header().Set("Content-Type", "application/json")
52-
w.Write([]byte(baseImpersonateCredsRespBody))
42+
w.Write([]byte(response))
5343
}))
54-
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
55-
targetServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44+
}
45+
46+
func createTargetServer(t *testing.T) *httptest.Server {
47+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
5648
if got, want := r.URL.String(), "/"; got != want {
5749
t.Errorf("URL.String(): got %v but want %v", got, want)
5850
}
@@ -74,27 +66,74 @@ func TestImpersonation(t *testing.T) {
7466
w.Header().Set("Content-Type", "application/json")
7567
w.Write([]byte(baseCredsResponseBody))
7668
}))
77-
defer targetServer.Close()
69+
}
7870

79-
testImpersonateConfig.TokenURL = targetServer.URL
80-
allURLs := regexp.MustCompile(".+")
81-
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
82-
if err != nil {
83-
t.Fatalf("Failed to create TokenSource: %v", err)
84-
}
71+
var impersonationTests = []struct {
72+
name string
73+
config Config
74+
expectedImpersonationBody string
75+
}{
76+
{
77+
name: "Base Impersonation",
78+
config: Config{
79+
Audience: "32555940559.apps.googleusercontent.com",
80+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
81+
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
82+
ClientSecret: "notsosecret",
83+
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
84+
CredentialSource: testBaseCredSource,
85+
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
86+
},
87+
expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
88+
},
89+
{
90+
name: "With TokenLifetime Set",
91+
config: Config{
92+
Audience: "32555940559.apps.googleusercontent.com",
93+
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
94+
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
95+
ClientSecret: "notsosecret",
96+
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
97+
CredentialSource: testBaseCredSource,
98+
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
99+
ServiceAccountImpersonationLifetimeSeconds: 10000,
100+
},
101+
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
102+
},
103+
}
85104

86-
oldNow := now
87-
defer func() { now = oldNow }()
88-
now = testNow
105+
func TestImpersonation(t *testing.T) {
106+
for _, tt := range impersonationTests {
107+
t.Run(tt.name, func(t *testing.T) {
108+
testImpersonateConfig := tt.config
109+
impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t)
110+
defer impersonateServer.Close()
111+
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
89112

90-
tok, err := ourTS.Token()
91-
if err != nil {
92-
t.Fatalf("Unexpected error: %e", err)
93-
}
94-
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
95-
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
96-
}
97-
if got, want := tok.TokenType, "Bearer"; got != want {
98-
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
113+
targetServer := createTargetServer(t)
114+
defer targetServer.Close()
115+
testImpersonateConfig.TokenURL = targetServer.URL
116+
117+
allURLs := regexp.MustCompile(".+")
118+
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
119+
if err != nil {
120+
t.Fatalf("Failed to create TokenSource: %v", err)
121+
}
122+
123+
oldNow := now
124+
defer func() { now = oldNow }()
125+
now = testNow
126+
127+
tok, err := ourTS.Token()
128+
if err != nil {
129+
t.Fatalf("Unexpected error: %e", err)
130+
}
131+
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
132+
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
133+
}
134+
if got, want := tok.TokenType, "Bearer"; got != want {
135+
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
136+
}
137+
})
99138
}
100139
}

0 commit comments

Comments
 (0)