Skip to content

Commit 7df4dd6

Browse files
gIthurielcodyoss
authored andcommitted
google/externalaccount: validate tokenURL and ServiceAccountImpersonationURL
Change-Id: Iab70cc967fd97ac8e349a14760df0f8b02ddf074 GitHub-Last-Rev: ddf4dbd GitHub-Pull-Request: #514 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/340569 Reviewed-by: Patrick Jones <[email protected]> Reviewed-by: Cody Oss <[email protected]> Reviewed-by: Chris Broadfoot <[email protected]> Trust: Cody Oss <[email protected]> Run-TryBot: Cody Oss <[email protected]> TryBot-Result: Go Bot <[email protected]>
1 parent faf39c7 commit 7df4dd6

11 files changed

+219
-27
lines changed

google/google.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
177177
QuotaProjectID: f.QuotaProjectID,
178178
Scopes: params.Scopes,
179179
}
180-
return cfg.TokenSource(ctx), nil
180+
return cfg.TokenSource(ctx)
181181
case "":
182182
return nil, errors.New("missing 'type' field in credentials")
183183
default:

google/internal/externalaccount/aws_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ func setTime(testTime time.Time) func() time.Time {
2828

2929
func setEnvironment(env map[string]string) func(string) string {
3030
return func(key string) string {
31-
value, _ := env[key]
32-
return value
31+
return env[key]
3332
}
3433
}
3534

@@ -650,7 +649,7 @@ func TestAwsCredential_BasicRequestWithDefaultEnv(t *testing.T) {
650649
getenv = setEnvironment(map[string]string{
651650
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
652651
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
653-
"AWS_DEFAULT_REGION": "us-west-1",
652+
"AWS_DEFAULT_REGION": "us-west-1",
654653
})
655654

656655
base, err := tfc.parse(context.Background())
@@ -688,7 +687,7 @@ func TestAwsCredential_BasicRequestWithTwoRegions(t *testing.T) {
688687
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
689688
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
690689
"AWS_REGION": "us-west-1",
691-
"AWS_DEFAULT_REGION": "us-east-1",
690+
"AWS_DEFAULT_REGION": "us-east-1",
692691
})
693692

694693
base, err := tfc.parse(context.Background())

google/internal/externalaccount/basecredentials.go

+78-16
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ package externalaccount
77
import (
88
"context"
99
"fmt"
10-
"golang.org/x/oauth2"
1110
"net/http"
11+
"net/url"
12+
"regexp"
1213
"strconv"
14+
"strings"
1315
"time"
16+
17+
"golang.org/x/oauth2"
1418
)
1519

1620
// now aliases time.Now for testing
@@ -22,43 +26,101 @@ var now = func() time.Time {
2226
type Config struct {
2327
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
2428
// identity pool or the workforce pool and the provider identifier in that pool.
25-
Audience string
29+
Audience string
2630
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
2731
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
28-
SubjectTokenType string
32+
SubjectTokenType string
2933
// TokenURL is the STS token exchange endpoint.
30-
TokenURL string
34+
TokenURL string
3135
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
3236
// user attributes like account identifier, eg. email, username, uid, etc). This is
3337
// needed for gCloud session account identification.
34-
TokenInfoURL string
38+
TokenInfoURL string
3539
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
3640
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
3741
ServiceAccountImpersonationURL string
3842
// ClientSecret is currently only required if token_info endpoint also
3943
// needs to be called with the generated GCP access token. When provided, STS will be
4044
// called with additional basic authentication using client_id as username and client_secret as password.
41-
ClientSecret string
45+
ClientSecret string
4246
// ClientID is only required in conjunction with ClientSecret, as described above.
43-
ClientID string
47+
ClientID string
4448
// CredentialSource contains the necessary information to retrieve the token itself, as well
4549
// as some environmental information.
46-
CredentialSource CredentialSource
50+
CredentialSource CredentialSource
4751
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
4852
// will set the x-goog-user-project which overrides the project associated with the credentials.
49-
QuotaProjectID string
53+
QuotaProjectID string
5054
// Scopes contains the desired scopes for the returned access token.
51-
Scopes []string
55+
Scopes []string
56+
}
57+
58+
// Each element consists of a list of patterns. validateURLs checks for matches
59+
// that include all elements in a given list, in that order.
60+
61+
var (
62+
validTokenURLPatterns = []*regexp.Regexp{
63+
// The complicated part in the middle matches any number of characters that
64+
// aren't period, spaces, or slashes.
65+
regexp.MustCompile(`(?i)^[^\.\s\/\\]+\.sts\.googleapis\.com$`),
66+
regexp.MustCompile(`(?i)^sts\.googleapis\.com$`),
67+
regexp.MustCompile(`(?i)^sts\.[^\.\s\/\\]+\.googleapis\.com$`),
68+
regexp.MustCompile(`(?i)^[^\.\s\/\\]+-sts\.googleapis\.com$`),
69+
}
70+
validImpersonateURLPatterns = []*regexp.Regexp{
71+
regexp.MustCompile(`^[^\.\s\/\\]+\.iamcredentials\.googleapis\.com$`),
72+
regexp.MustCompile(`^iamcredentials\.googleapis\.com$`),
73+
regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
74+
regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
75+
}
76+
)
77+
78+
func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
79+
parsed, err := url.Parse(input)
80+
if err != nil {
81+
return false
82+
}
83+
if !strings.EqualFold(parsed.Scheme, scheme) {
84+
return false
85+
}
86+
toTest := parsed.Host
87+
88+
for _, pattern := range patterns {
89+
90+
if valid := pattern.MatchString(toTest); valid {
91+
return true
92+
}
93+
}
94+
return false
5295
}
5396

5497
// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
55-
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
98+
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
99+
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
100+
}
101+
102+
// tokenSource is a private function that's directly called by some of the tests,
103+
// because the unit test URLs are mocked, and would otherwise fail the
104+
// validity check.
105+
func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Regexp, impersonateURLValidPats []*regexp.Regexp, scheme string) (oauth2.TokenSource, error) {
106+
valid := validateURL(c.TokenURL, tokenURLValidPats, scheme)
107+
if !valid {
108+
return nil, fmt.Errorf("oauth2/google: invalid TokenURL provided while constructing tokenSource")
109+
}
110+
111+
if c.ServiceAccountImpersonationURL != "" {
112+
valid := validateURL(c.ServiceAccountImpersonationURL, impersonateURLValidPats, scheme)
113+
if !valid {
114+
return nil, fmt.Errorf("oauth2/google: invalid ServiceAccountImpersonationURL provided while constructing tokenSource")
115+
}
116+
}
117+
56118
ts := tokenSource{
57119
ctx: ctx,
58120
conf: c,
59121
}
60122
if c.ServiceAccountImpersonationURL == "" {
61-
return oauth2.ReuseTokenSource(nil, ts)
123+
return oauth2.ReuseTokenSource(nil, ts), nil
62124
}
63125
scopes := c.Scopes
64126
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
@@ -68,7 +130,7 @@ func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
68130
scopes: scopes,
69131
ts: oauth2.ReuseTokenSource(nil, ts),
70132
}
71-
return oauth2.ReuseTokenSource(nil, imp)
133+
return oauth2.ReuseTokenSource(nil, imp), nil
72134
}
73135

74136
// Subject token file types.
@@ -78,9 +140,9 @@ const (
78140
)
79141

80142
type format struct {
81-
// Type is either "text" or "json". When not provided "text" type is assumed.
143+
// Type is either "text" or "json". When not provided "text" type is assumed.
82144
Type string `json:"type"`
83-
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
145+
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
84146
SubjectTokenFieldName string `json:"subject_token_field_name"`
85147
}
86148

@@ -128,7 +190,7 @@ type baseCredentialSource interface {
128190
subjectToken() (string, error)
129191
}
130192

131-
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
193+
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
132194
type tokenSource struct {
133195
ctx context.Context
134196
conf *Config

google/internal/externalaccount/basecredentials_test.go

+115
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io/ioutil"
1010
"net/http"
1111
"net/http/httptest"
12+
"strings"
1213
"testing"
1314
"time"
1415
)
@@ -95,3 +96,117 @@ func TestToken(t *testing.T) {
9596
}
9697

9798
}
99+
100+
func TestValidateURLTokenURL(t *testing.T) {
101+
var urlValidityTests = []struct {
102+
tokURL string
103+
expectSuccess bool
104+
}{
105+
{"https://east.sts.googleapis.com", true},
106+
{"https://sts.googleapis.com", true},
107+
{"https://sts.asfeasfesef.googleapis.com", true},
108+
{"https://us-east-1-sts.googleapis.com", true},
109+
{"https://sts.googleapis.com/your/path/here", true},
110+
{"https://.sts.googleapis.com", false},
111+
{"https://badsts.googleapis.com", false},
112+
{"https://sts.asfe.asfesef.googleapis.com", false},
113+
{"https://sts..googleapis.com", false},
114+
{"https://-sts.googleapis.com", false},
115+
{"https://us-ea.st-1-sts.googleapis.com", false},
116+
{"https://sts.googleapis.com.evil.com/whatever/path", false},
117+
{"https://us-eas\\t-1.sts.googleapis.com", false},
118+
{"https:/us-ea/st-1.sts.googleapis.com", false},
119+
{"https:/us-east 1.sts.googleapis.com", false},
120+
{"https://", false},
121+
{"http://us-east-1.sts.googleapis.com", false},
122+
{"https://us-east-1.sts.googleapis.comevil.com", false},
123+
}
124+
ctx := context.Background()
125+
for _, tt := range urlValidityTests {
126+
t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
127+
config := testConfig
128+
config.TokenURL = tt.tokURL
129+
_, err := config.TokenSource(ctx)
130+
131+
if tt.expectSuccess && err != nil {
132+
t.Errorf("got %v but want nil", err)
133+
} else if !tt.expectSuccess && err == nil {
134+
t.Errorf("got nil but expected an error")
135+
}
136+
})
137+
}
138+
for _, el := range urlValidityTests {
139+
el.tokURL = strings.ToUpper(el.tokURL)
140+
}
141+
for _, tt := range urlValidityTests {
142+
t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
143+
config := testConfig
144+
config.TokenURL = tt.tokURL
145+
_, err := config.TokenSource(ctx)
146+
147+
if tt.expectSuccess && err != nil {
148+
t.Errorf("got %v but want nil", err)
149+
} else if !tt.expectSuccess && err == nil {
150+
t.Errorf("got nil but expected an error")
151+
}
152+
})
153+
}
154+
}
155+
156+
func TestValidateURLImpersonateURL(t *testing.T) {
157+
var urlValidityTests = []struct {
158+
impURL string
159+
expectSuccess bool
160+
}{
161+
{"https://east.iamcredentials.googleapis.com", true},
162+
{"https://iamcredentials.googleapis.com", true},
163+
{"https://iamcredentials.asfeasfesef.googleapis.com", true},
164+
{"https://us-east-1-iamcredentials.googleapis.com", true},
165+
{"https://iamcredentials.googleapis.com/your/path/here", true},
166+
{"https://.iamcredentials.googleapis.com", false},
167+
{"https://badiamcredentials.googleapis.com", false},
168+
{"https://iamcredentials.asfe.asfesef.googleapis.com", false},
169+
{"https://iamcredentials..googleapis.com", false},
170+
{"https://-iamcredentials.googleapis.com", false},
171+
{"https://us-ea.st-1-iamcredentials.googleapis.com", false},
172+
{"https://iamcredentials.googleapis.com.evil.com/whatever/path", false},
173+
{"https://us-eas\\t-1.iamcredentials.googleapis.com", false},
174+
{"https:/us-ea/st-1.iamcredentials.googleapis.com", false},
175+
{"https:/us-east 1.iamcredentials.googleapis.com", false},
176+
{"https://", false},
177+
{"http://us-east-1.iamcredentials.googleapis.com", false},
178+
{"https://us-east-1.iamcredentials.googleapis.comevil.com", false},
179+
}
180+
ctx := context.Background()
181+
for _, tt := range urlValidityTests {
182+
t.Run(" "+tt.impURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
183+
config := testConfig
184+
config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
185+
config.ServiceAccountImpersonationURL = tt.impURL
186+
_, err := config.TokenSource(ctx)
187+
188+
if tt.expectSuccess && err != nil {
189+
t.Errorf("got %v but want nil", err)
190+
} else if !tt.expectSuccess && err == nil {
191+
t.Errorf("got nil but expected an error")
192+
}
193+
})
194+
}
195+
for _, el := range urlValidityTests {
196+
el.impURL = strings.ToUpper(el.impURL)
197+
}
198+
for _, tt := range urlValidityTests {
199+
t.Run(" "+tt.impURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
200+
config := testConfig
201+
config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
202+
config.ServiceAccountImpersonationURL = tt.impURL
203+
_, err := config.TokenSource(ctx)
204+
205+
if tt.expectSuccess && err != nil {
206+
t.Errorf("got %v but want nil", err)
207+
} else if !tt.expectSuccess && err == nil {
208+
t.Errorf("got nil but expected an error")
209+
}
210+
})
211+
}
212+
}

google/internal/externalaccount/clientauth.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ package externalaccount
66

77
import (
88
"encoding/base64"
9-
"golang.org/x/oauth2"
109
"net/http"
1110
"net/url"
11+
12+
"golang.org/x/oauth2"
1213
)
1314

1415
// clientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1.

google/internal/externalaccount/clientauth_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
package externalaccount
66

77
import (
8-
"golang.org/x/oauth2"
98
"net/http"
109
"net/url"
1110
"reflect"
1211
"testing"
12+
13+
"golang.org/x/oauth2"
1314
)
1415

1516
var clientID = "rbrgnognrhongo3bi4gb9ghg9g"

google/internal/externalaccount/impersonate.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import (
99
"context"
1010
"encoding/json"
1111
"fmt"
12-
"golang.org/x/oauth2"
1312
"io"
1413
"io/ioutil"
1514
"net/http"
1615
"time"
16+
17+
"golang.org/x/oauth2"
1718
)
1819

1920
// generateAccesstokenReq is used for service account impersonation

google/internal/externalaccount/impersonate_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io/ioutil"
1010
"net/http"
1111
"net/http/httptest"
12+
"regexp"
1213
"testing"
1314
)
1415

@@ -76,7 +77,11 @@ func TestImpersonation(t *testing.T) {
7677
defer targetServer.Close()
7778

7879
testImpersonateConfig.TokenURL = targetServer.URL
79-
ourTS := testImpersonateConfig.TokenSource(context.Background())
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+
}
8085

8186
oldNow := now
8287
defer func() { now = oldNow }()

google/internal/externalaccount/sts_exchange.go

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchan
6565
defer resp.Body.Close()
6666

6767
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
68+
if err != nil {
69+
return nil, err
70+
}
6871
if c := resp.StatusCode; c < 200 || c > 299 {
6972
return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body)
7073
}

0 commit comments

Comments
 (0)