Skip to content

Commit 43b6a7b

Browse files
BigTailWolfcodyoss
authored andcommitted
google: adding support for external account authorized user
To support a new type of credential: `ExternalAccountAuthorizedUser` * Refactor the common dependency STS to a separate package. * Adding the `externalaccountauthorizeduser` package. Change-Id: I9b9624f912d216b67a0d31945a50f057f747710b GitHub-Last-Rev: 6e2aaff GitHub-Pull-Request: #671 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/531095 Reviewed-by: Leo Siracusa <[email protected]> Reviewed-by: Alex Eitzman <[email protected]> Run-TryBot: Cody Oss <[email protected]> Reviewed-by: Cody Oss <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 14b275c commit 43b6a7b

File tree

8 files changed

+534
-63
lines changed

8 files changed

+534
-63
lines changed

google/google.go

+22-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"cloud.google.com/go/compute/metadata"
1717
"golang.org/x/oauth2"
1818
"golang.org/x/oauth2/google/internal/externalaccount"
19+
"golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
1920
"golang.org/x/oauth2/jwt"
2021
)
2122

@@ -96,10 +97,11 @@ func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
9697

9798
// JSON key file types.
9899
const (
99-
serviceAccountKey = "service_account"
100-
userCredentialsKey = "authorized_user"
101-
externalAccountKey = "external_account"
102-
impersonatedServiceAccount = "impersonated_service_account"
100+
serviceAccountKey = "service_account"
101+
userCredentialsKey = "authorized_user"
102+
externalAccountKey = "external_account"
103+
externalAccountAuthorizedUserKey = "external_account_authorized_user"
104+
impersonatedServiceAccount = "impersonated_service_account"
103105
)
104106

105107
// credentialsFile is the unmarshalled representation of a credentials file.
@@ -132,6 +134,9 @@ type credentialsFile struct {
132134
QuotaProjectID string `json:"quota_project_id"`
133135
WorkforcePoolUserProject string `json:"workforce_pool_user_project"`
134136

137+
// External Account Authorized User fields
138+
RevokeURL string `json:"revoke_url"`
139+
135140
// Service account impersonation
136141
SourceCredentials *credentialsFile `json:"source_credentials"`
137142
}
@@ -200,6 +205,19 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
200205
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
201206
}
202207
return cfg.TokenSource(ctx)
208+
case externalAccountAuthorizedUserKey:
209+
cfg := &externalaccountauthorizeduser.Config{
210+
Audience: f.Audience,
211+
RefreshToken: f.RefreshToken,
212+
TokenURL: f.TokenURLExternal,
213+
TokenInfoURL: f.TokenInfoURL,
214+
ClientID: f.ClientID,
215+
ClientSecret: f.ClientSecret,
216+
RevokeURL: f.RevokeURL,
217+
QuotaProjectID: f.QuotaProjectID,
218+
Scopes: params.Scopes,
219+
}
220+
return cfg.TokenSource(ctx)
203221
case impersonatedServiceAccount:
204222
if f.ServiceAccountImpersonationURL == "" || f.SourceCredentials == nil {
205223
return nil, errors.New("missing 'source_credentials' field or 'service_account_impersonation_url' in credentials")

google/internal/externalaccount/basecredentials.go

+4-26
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import (
88
"context"
99
"fmt"
1010
"net/http"
11-
"net/url"
1211
"regexp"
1312
"strconv"
14-
"strings"
1513
"time"
1614

1715
"golang.org/x/oauth2"
16+
"golang.org/x/oauth2/google/internal/stsexchange"
1817
)
1918

2019
// now aliases time.Now for testing
@@ -63,31 +62,10 @@ type Config struct {
6362
WorkforcePoolUserProject string
6463
}
6564

66-
// Each element consists of a list of patterns. validateURLs checks for matches
67-
// that include all elements in a given list, in that order.
68-
6965
var (
7066
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
7167
)
7268

73-
func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
74-
parsed, err := url.Parse(input)
75-
if err != nil {
76-
return false
77-
}
78-
if !strings.EqualFold(parsed.Scheme, scheme) {
79-
return false
80-
}
81-
toTest := parsed.Host
82-
83-
for _, pattern := range patterns {
84-
if pattern.MatchString(toTest) {
85-
return true
86-
}
87-
}
88-
return false
89-
}
90-
9169
func validateWorkforceAudience(input string) bool {
9270
return validWorkforceAudiencePattern.MatchString(input)
9371
}
@@ -230,7 +208,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
230208
if err != nil {
231209
return nil, err
232210
}
233-
stsRequest := stsTokenExchangeRequest{
211+
stsRequest := stsexchange.TokenExchangeRequest{
234212
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
235213
Audience: conf.Audience,
236214
Scope: conf.Scopes,
@@ -241,7 +219,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
241219
header := make(http.Header)
242220
header.Add("Content-Type", "application/x-www-form-urlencoded")
243221
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
244-
clientAuth := clientAuthentication{
222+
clientAuth := stsexchange.ClientAuthentication{
245223
AuthStyle: oauth2.AuthStyleInHeader,
246224
ClientID: conf.ClientID,
247225
ClientSecret: conf.ClientSecret,
@@ -254,7 +232,7 @@ func (ts tokenSource) Token() (*oauth2.Token, error) {
254232
"userProject": conf.WorkforcePoolUserProject,
255233
}
256234
}
257-
stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
235+
stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
258236
if err != nil {
259237
return nil, err
260238
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2023 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package externalaccountauthorizeduser
6+
7+
import (
8+
"context"
9+
"errors"
10+
"time"
11+
12+
"golang.org/x/oauth2"
13+
"golang.org/x/oauth2/google/internal/stsexchange"
14+
)
15+
16+
// now aliases time.Now for testing.
17+
var now = func() time.Time {
18+
return time.Now().UTC()
19+
}
20+
21+
var tokenValid = func(token oauth2.Token) bool {
22+
return token.Valid()
23+
}
24+
25+
type Config struct {
26+
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workforce pool and
27+
// the provider identifier in that pool.
28+
Audience string
29+
// RefreshToken is the optional OAuth 2.0 refresh token. If specified, credentials can be refreshed.
30+
RefreshToken string
31+
// TokenURL is the optional STS token exchange endpoint for refresh. Must be specified for refresh, can be left as
32+
// None if the token can not be refreshed.
33+
TokenURL string
34+
// TokenInfoURL is the optional STS endpoint URL for token introspection.
35+
TokenInfoURL string
36+
// ClientID is only required in conjunction with ClientSecret, as described above.
37+
ClientID string
38+
// ClientSecret is currently only required if token_info endpoint also needs to be called with the generated GCP
39+
// access token. When provided, STS will be called with additional basic authentication using client_id as username
40+
// and client_secret as password.
41+
ClientSecret string
42+
// Token is the OAuth2.0 access token. Can be nil if refresh information is provided.
43+
Token string
44+
// Expiry is the optional expiration datetime of the OAuth 2.0 access token.
45+
Expiry time.Time
46+
// RevokeURL is the optional STS endpoint URL for revoking tokens.
47+
RevokeURL string
48+
// QuotaProjectID is the optional project ID used for quota and billing. This project may be different from the
49+
// project used to create the credentials.
50+
QuotaProjectID string
51+
Scopes []string
52+
}
53+
54+
func (c *Config) canRefresh() bool {
55+
return c.ClientID != "" && c.ClientSecret != "" && c.RefreshToken != "" && c.TokenURL != ""
56+
}
57+
58+
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
59+
var token oauth2.Token
60+
if c.Token != "" && !c.Expiry.IsZero() {
61+
token = oauth2.Token{
62+
AccessToken: c.Token,
63+
Expiry: c.Expiry,
64+
TokenType: "Bearer",
65+
}
66+
}
67+
if !tokenValid(token) && !c.canRefresh() {
68+
return nil, errors.New("oauth2/google: Token should be created with fields to make it valid (`token` and `expiry`), or fields to allow it to refresh (`refresh_token`, `token_url`, `client_id`, `client_secret`).")
69+
}
70+
71+
ts := tokenSource{
72+
ctx: ctx,
73+
conf: c,
74+
}
75+
76+
return oauth2.ReuseTokenSource(&token, ts), nil
77+
}
78+
79+
type tokenSource struct {
80+
ctx context.Context
81+
conf *Config
82+
}
83+
84+
func (ts tokenSource) Token() (*oauth2.Token, error) {
85+
conf := ts.conf
86+
if !conf.canRefresh() {
87+
return nil, errors.New("oauth2/google: The credentials do not contain the necessary fields need to refresh the access token. You must specify refresh_token, token_url, client_id, and client_secret.")
88+
}
89+
90+
clientAuth := stsexchange.ClientAuthentication{
91+
AuthStyle: oauth2.AuthStyleInHeader,
92+
ClientID: conf.ClientID,
93+
ClientSecret: conf.ClientSecret,
94+
}
95+
96+
stsResponse, err := stsexchange.RefreshAccessToken(ts.ctx, conf.TokenURL, conf.RefreshToken, clientAuth, nil)
97+
if err != nil {
98+
return nil, err
99+
}
100+
if stsResponse.ExpiresIn < 0 {
101+
return nil, errors.New("oauth2/google: got invalid expiry from security token service")
102+
}
103+
104+
if stsResponse.RefreshToken != "" {
105+
conf.RefreshToken = stsResponse.RefreshToken
106+
}
107+
108+
token := &oauth2.Token{
109+
AccessToken: stsResponse.AccessToken,
110+
Expiry: now().Add(time.Duration(stsResponse.ExpiresIn) * time.Second),
111+
TokenType: "Bearer",
112+
}
113+
return token, nil
114+
}

0 commit comments

Comments
 (0)