Codebase list golang-golang-x-oauth2 / 7df4dd6
google/externalaccount: validate tokenURL and ServiceAccountImpersonationURL Change-Id: Iab70cc967fd97ac8e349a14760df0f8b02ddf074 GitHub-Last-Rev: ddf4dbd0b7096a0d34677047b9c3992bb6ed359b GitHub-Pull-Request: golang/oauth2#514 Reviewed-on: https://go-review.googlesource.com/c/oauth2/+/340569 Reviewed-by: Patrick Jones <ithuriel@google.com> Reviewed-by: Cody Oss <codyoss@google.com> Reviewed-by: Chris Broadfoot <cbro@golang.org> Trust: Cody Oss <codyoss@google.com> Run-TryBot: Cody Oss <codyoss@google.com> TryBot-Result: Go Bot <gobot@golang.org> Patrick Jones authored 2 years ago Cody Oss committed 2 years ago
11 changed file(s) with 221 addition(s) and 29 deletion(s). Raw diff Collapse all Expand all
176176 QuotaProjectID: f.QuotaProjectID,
177177 Scopes: params.Scopes,
178178 }
179 return cfg.TokenSource(ctx), nil
179 return cfg.TokenSource(ctx)
180180 case "":
181181 return nil, errors.New("missing 'type' field in credentials")
182182 default:
2727
2828 func setEnvironment(env map[string]string) func(string) string {
2929 return func(key string) string {
30 value, _ := env[key]
31 return value
30 return env[key]
3231 }
3332 }
3433
649648 getenv = setEnvironment(map[string]string{
650649 "AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
651650 "AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
652 "AWS_DEFAULT_REGION": "us-west-1",
651 "AWS_DEFAULT_REGION": "us-west-1",
653652 })
654653
655654 base, err := tfc.parse(context.Background())
687686 "AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
688687 "AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
689688 "AWS_REGION": "us-west-1",
690 "AWS_DEFAULT_REGION": "us-east-1",
689 "AWS_DEFAULT_REGION": "us-east-1",
691690 })
692691
693692 base, err := tfc.parse(context.Background())
66 import (
77 "context"
88 "fmt"
9 "net/http"
10 "net/url"
11 "regexp"
12 "strconv"
13 "strings"
14 "time"
15
916 "golang.org/x/oauth2"
10 "net/http"
11 "strconv"
12 "time"
1317 )
1418
1519 // now aliases time.Now for testing
2125 type Config struct {
2226 // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
2327 // identity pool or the workforce pool and the provider identifier in that pool.
24 Audience string
28 Audience string
2529 // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
2630 // e.g. `urn:ietf:params:oauth:token-type:jwt`.
27 SubjectTokenType string
31 SubjectTokenType string
2832 // TokenURL is the STS token exchange endpoint.
29 TokenURL string
33 TokenURL string
3034 // TokenInfoURL is the token_info endpoint used to retrieve the account related information (
3135 // user attributes like account identifier, eg. email, username, uid, etc). This is
3236 // needed for gCloud session account identification.
33 TokenInfoURL string
37 TokenInfoURL string
3438 // ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
3539 // required for workload identity pools when APIs to be accessed have not integrated with UberMint.
3640 ServiceAccountImpersonationURL string
3741 // ClientSecret is currently only required if token_info endpoint also
3842 // needs to be called with the generated GCP access token. When provided, STS will be
3943 // called with additional basic authentication using client_id as username and client_secret as password.
40 ClientSecret string
44 ClientSecret string
4145 // ClientID is only required in conjunction with ClientSecret, as described above.
42 ClientID string
46 ClientID string
4347 // CredentialSource contains the necessary information to retrieve the token itself, as well
4448 // as some environmental information.
45 CredentialSource CredentialSource
49 CredentialSource CredentialSource
4650 // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
4751 // will set the x-goog-user-project which overrides the project associated with the credentials.
48 QuotaProjectID string
52 QuotaProjectID string
4953 // Scopes contains the desired scopes for the returned access token.
50 Scopes []string
54 Scopes []string
55 }
56
57 // Each element consists of a list of patterns. validateURLs checks for matches
58 // that include all elements in a given list, in that order.
59
60 var (
61 validTokenURLPatterns = []*regexp.Regexp{
62 // The complicated part in the middle matches any number of characters that
63 // aren't period, spaces, or slashes.
64 regexp.MustCompile(`(?i)^[^\.\s\/\\]+\.sts\.googleapis\.com$`),
65 regexp.MustCompile(`(?i)^sts\.googleapis\.com$`),
66 regexp.MustCompile(`(?i)^sts\.[^\.\s\/\\]+\.googleapis\.com$`),
67 regexp.MustCompile(`(?i)^[^\.\s\/\\]+-sts\.googleapis\.com$`),
68 }
69 validImpersonateURLPatterns = []*regexp.Regexp{
70 regexp.MustCompile(`^[^\.\s\/\\]+\.iamcredentials\.googleapis\.com$`),
71 regexp.MustCompile(`^iamcredentials\.googleapis\.com$`),
72 regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
73 regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
74 }
75 )
76
77 func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
78 parsed, err := url.Parse(input)
79 if err != nil {
80 return false
81 }
82 if !strings.EqualFold(parsed.Scheme, scheme) {
83 return false
84 }
85 toTest := parsed.Host
86
87 for _, pattern := range patterns {
88
89 if valid := pattern.MatchString(toTest); valid {
90 return true
91 }
92 }
93 return false
5194 }
5295
5396 // TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
54 func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
97 func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
98 return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
99 }
100
101 // tokenSource is a private function that's directly called by some of the tests,
102 // because the unit test URLs are mocked, and would otherwise fail the
103 // validity check.
104 func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Regexp, impersonateURLValidPats []*regexp.Regexp, scheme string) (oauth2.TokenSource, error) {
105 valid := validateURL(c.TokenURL, tokenURLValidPats, scheme)
106 if !valid {
107 return nil, fmt.Errorf("oauth2/google: invalid TokenURL provided while constructing tokenSource")
108 }
109
110 if c.ServiceAccountImpersonationURL != "" {
111 valid := validateURL(c.ServiceAccountImpersonationURL, impersonateURLValidPats, scheme)
112 if !valid {
113 return nil, fmt.Errorf("oauth2/google: invalid ServiceAccountImpersonationURL provided while constructing tokenSource")
114 }
115 }
116
55117 ts := tokenSource{
56118 ctx: ctx,
57119 conf: c,
58120 }
59121 if c.ServiceAccountImpersonationURL == "" {
60 return oauth2.ReuseTokenSource(nil, ts)
122 return oauth2.ReuseTokenSource(nil, ts), nil
61123 }
62124 scopes := c.Scopes
63125 ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
67129 scopes: scopes,
68130 ts: oauth2.ReuseTokenSource(nil, ts),
69131 }
70 return oauth2.ReuseTokenSource(nil, imp)
132 return oauth2.ReuseTokenSource(nil, imp), nil
71133 }
72134
73135 // Subject token file types.
77139 )
78140
79141 type format struct {
80 // Type is either "text" or "json". When not provided "text" type is assumed.
142 // Type is either "text" or "json". When not provided "text" type is assumed.
81143 Type string `json:"type"`
82 // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
144 // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
83145 SubjectTokenFieldName string `json:"subject_token_field_name"`
84146 }
85147
127189 subjectToken() (string, error)
128190 }
129191
130 // tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
192 // tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
131193 type tokenSource struct {
132194 ctx context.Context
133195 conf *Config
88 "io/ioutil"
99 "net/http"
1010 "net/http/httptest"
11 "strings"
1112 "testing"
1213 "time"
1314 )
9495 }
9596
9697 }
98
99 func TestValidateURLTokenURL(t *testing.T) {
100 var urlValidityTests = []struct {
101 tokURL string
102 expectSuccess bool
103 }{
104 {"https://east.sts.googleapis.com", true},
105 {"https://sts.googleapis.com", true},
106 {"https://sts.asfeasfesef.googleapis.com", true},
107 {"https://us-east-1-sts.googleapis.com", true},
108 {"https://sts.googleapis.com/your/path/here", true},
109 {"https://.sts.googleapis.com", false},
110 {"https://badsts.googleapis.com", false},
111 {"https://sts.asfe.asfesef.googleapis.com", false},
112 {"https://sts..googleapis.com", false},
113 {"https://-sts.googleapis.com", false},
114 {"https://us-ea.st-1-sts.googleapis.com", false},
115 {"https://sts.googleapis.com.evil.com/whatever/path", false},
116 {"https://us-eas\\t-1.sts.googleapis.com", false},
117 {"https:/us-ea/st-1.sts.googleapis.com", false},
118 {"https:/us-east 1.sts.googleapis.com", false},
119 {"https://", false},
120 {"http://us-east-1.sts.googleapis.com", false},
121 {"https://us-east-1.sts.googleapis.comevil.com", false},
122 }
123 ctx := context.Background()
124 for _, tt := range urlValidityTests {
125 t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
126 config := testConfig
127 config.TokenURL = tt.tokURL
128 _, err := config.TokenSource(ctx)
129
130 if tt.expectSuccess && err != nil {
131 t.Errorf("got %v but want nil", err)
132 } else if !tt.expectSuccess && err == nil {
133 t.Errorf("got nil but expected an error")
134 }
135 })
136 }
137 for _, el := range urlValidityTests {
138 el.tokURL = strings.ToUpper(el.tokURL)
139 }
140 for _, tt := range urlValidityTests {
141 t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
142 config := testConfig
143 config.TokenURL = tt.tokURL
144 _, err := config.TokenSource(ctx)
145
146 if tt.expectSuccess && err != nil {
147 t.Errorf("got %v but want nil", err)
148 } else if !tt.expectSuccess && err == nil {
149 t.Errorf("got nil but expected an error")
150 }
151 })
152 }
153 }
154
155 func TestValidateURLImpersonateURL(t *testing.T) {
156 var urlValidityTests = []struct {
157 impURL string
158 expectSuccess bool
159 }{
160 {"https://east.iamcredentials.googleapis.com", true},
161 {"https://iamcredentials.googleapis.com", true},
162 {"https://iamcredentials.asfeasfesef.googleapis.com", true},
163 {"https://us-east-1-iamcredentials.googleapis.com", true},
164 {"https://iamcredentials.googleapis.com/your/path/here", true},
165 {"https://.iamcredentials.googleapis.com", false},
166 {"https://badiamcredentials.googleapis.com", false},
167 {"https://iamcredentials.asfe.asfesef.googleapis.com", false},
168 {"https://iamcredentials..googleapis.com", false},
169 {"https://-iamcredentials.googleapis.com", false},
170 {"https://us-ea.st-1-iamcredentials.googleapis.com", false},
171 {"https://iamcredentials.googleapis.com.evil.com/whatever/path", false},
172 {"https://us-eas\\t-1.iamcredentials.googleapis.com", false},
173 {"https:/us-ea/st-1.iamcredentials.googleapis.com", false},
174 {"https:/us-east 1.iamcredentials.googleapis.com", false},
175 {"https://", false},
176 {"http://us-east-1.iamcredentials.googleapis.com", false},
177 {"https://us-east-1.iamcredentials.googleapis.comevil.com", false},
178 }
179 ctx := context.Background()
180 for _, tt := range urlValidityTests {
181 t.Run(" "+tt.impURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
182 config := testConfig
183 config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
184 config.ServiceAccountImpersonationURL = tt.impURL
185 _, err := config.TokenSource(ctx)
186
187 if tt.expectSuccess && err != nil {
188 t.Errorf("got %v but want nil", err)
189 } else if !tt.expectSuccess && err == nil {
190 t.Errorf("got nil but expected an error")
191 }
192 })
193 }
194 for _, el := range urlValidityTests {
195 el.impURL = strings.ToUpper(el.impURL)
196 }
197 for _, tt := range urlValidityTests {
198 t.Run(" "+tt.impURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
199 config := testConfig
200 config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
201 config.ServiceAccountImpersonationURL = tt.impURL
202 _, err := config.TokenSource(ctx)
203
204 if tt.expectSuccess && err != nil {
205 t.Errorf("got %v but want nil", err)
206 } else if !tt.expectSuccess && err == nil {
207 t.Errorf("got nil but expected an error")
208 }
209 })
210 }
211 }
55
66 import (
77 "encoding/base64"
8 "golang.org/x/oauth2"
98 "net/http"
109 "net/url"
10
11 "golang.org/x/oauth2"
1112 )
1213
1314 // clientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1.
44 package externalaccount
55
66 import (
7 "golang.org/x/oauth2"
87 "net/http"
98 "net/url"
109 "reflect"
1110 "testing"
11
12 "golang.org/x/oauth2"
1213 )
1314
1415 var clientID = "rbrgnognrhongo3bi4gb9ghg9g"
88 "context"
99 "encoding/json"
1010 "fmt"
11 "golang.org/x/oauth2"
1211 "io"
1312 "io/ioutil"
1413 "net/http"
1514 "time"
15
16 "golang.org/x/oauth2"
1617 )
1718
1819 // generateAccesstokenReq is used for service account impersonation
88 "io/ioutil"
99 "net/http"
1010 "net/http/httptest"
11 "regexp"
1112 "testing"
1213 )
1314
7576 defer targetServer.Close()
7677
7778 testImpersonateConfig.TokenURL = targetServer.URL
78 ourTS := testImpersonateConfig.TokenSource(context.Background())
79 allURLs := regexp.MustCompile(".+")
80 ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
81 if err != nil {
82 t.Fatalf("Failed to create TokenSource: %v", err)
83 }
7984
8085 oldNow := now
8186 defer func() { now = oldNow }()
6464 defer resp.Body.Close()
6565
6666 body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
67 if err != nil {
68 return nil, err
69 }
6770 if c := resp.StatusCode; c < 200 || c > 299 {
6871 return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body)
6972 }
66 import (
77 "context"
88 "encoding/json"
9 "golang.org/x/oauth2"
109 "io/ioutil"
1110 "net/http"
1211 "net/http/httptest"
1312 "net/url"
1413 "testing"
14
15 "golang.org/x/oauth2"
1516 )
1617
1718 var auth = clientAuthentication{
126127 }
127128 var opts map[string]interface{}
128129 err = json.Unmarshal([]byte(strOpts[0]), &opts)
130 if err != nil {
131 t.Fatalf("Couldn't parse received \"options\" field.")
132 }
129133 if len(opts) < 2 {
130134 t.Errorf("Too few options received.")
131135 }
88 "encoding/json"
99 "errors"
1010 "fmt"
11 "golang.org/x/oauth2"
1211 "io"
1312 "io/ioutil"
1413 "net/http"
14
15 "golang.org/x/oauth2"
1516 )
1617
1718 type urlCredentialSource struct {