Skip to content

Commit

Permalink
google/externalaccount: validate tokenURL and ServiceAccountImpersona…
Browse files Browse the repository at this point in the history
…tionURL

Change-Id: Iab70cc967fd97ac8e349a14760df0f8b02ddf074
GitHub-Last-Rev: ddf4dbd
GitHub-Pull-Request: golang#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]>
  • Loading branch information
gIthuriel authored and codyoss committed Aug 17, 2021
1 parent faf39c7 commit 7df4dd6
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 27 deletions.
2 changes: 1 addition & 1 deletion google/google.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
QuotaProjectID: f.QuotaProjectID,
Scopes: params.Scopes,
}
return cfg.TokenSource(ctx), nil
return cfg.TokenSource(ctx)
case "":
return nil, errors.New("missing 'type' field in credentials")
default:
Expand Down
7 changes: 3 additions & 4 deletions google/internal/externalaccount/aws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ func setTime(testTime time.Time) func() time.Time {

func setEnvironment(env map[string]string) func(string) string {
return func(key string) string {
value, _ := env[key]
return value
return env[key]
}
}

Expand Down Expand Up @@ -650,7 +649,7 @@ func TestAwsCredential_BasicRequestWithDefaultEnv(t *testing.T) {
getenv = setEnvironment(map[string]string{
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"AWS_DEFAULT_REGION": "us-west-1",
"AWS_DEFAULT_REGION": "us-west-1",
})

base, err := tfc.parse(context.Background())
Expand Down Expand Up @@ -688,7 +687,7 @@ func TestAwsCredential_BasicRequestWithTwoRegions(t *testing.T) {
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"AWS_REGION": "us-west-1",
"AWS_DEFAULT_REGION": "us-east-1",
"AWS_DEFAULT_REGION": "us-east-1",
})

base, err := tfc.parse(context.Background())
Expand Down
94 changes: 78 additions & 16 deletions google/internal/externalaccount/basecredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ package externalaccount
import (
"context"
"fmt"
"golang.org/x/oauth2"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"

"golang.org/x/oauth2"
)

// now aliases time.Now for testing
Expand All @@ -22,43 +26,101 @@ var now = func() time.Time {
type Config struct {
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
// identity pool or the workforce pool and the provider identifier in that pool.
Audience string
Audience string
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
SubjectTokenType string
SubjectTokenType string
// TokenURL is the STS token exchange endpoint.
TokenURL string
TokenURL string
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
// user attributes like account identifier, eg. email, username, uid, etc). This is
// needed for gCloud session account identification.
TokenInfoURL string
TokenInfoURL string
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
ServiceAccountImpersonationURL string
// ClientSecret is currently only required if token_info endpoint also
// needs to be called with the generated GCP access token. When provided, STS will be
// called with additional basic authentication using client_id as username and client_secret as password.
ClientSecret string
ClientSecret string
// ClientID is only required in conjunction with ClientSecret, as described above.
ClientID string
ClientID string
// CredentialSource contains the necessary information to retrieve the token itself, as well
// as some environmental information.
CredentialSource CredentialSource
CredentialSource CredentialSource
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
// will set the x-goog-user-project which overrides the project associated with the credentials.
QuotaProjectID string
QuotaProjectID string
// Scopes contains the desired scopes for the returned access token.
Scopes []string
Scopes []string
}

// Each element consists of a list of patterns. validateURLs checks for matches
// that include all elements in a given list, in that order.

var (
validTokenURLPatterns = []*regexp.Regexp{
// The complicated part in the middle matches any number of characters that
// aren't period, spaces, or slashes.
regexp.MustCompile(`(?i)^[^\.\s\/\\]+\.sts\.googleapis\.com$`),
regexp.MustCompile(`(?i)^sts\.googleapis\.com$`),
regexp.MustCompile(`(?i)^sts\.[^\.\s\/\\]+\.googleapis\.com$`),
regexp.MustCompile(`(?i)^[^\.\s\/\\]+-sts\.googleapis\.com$`),
}
validImpersonateURLPatterns = []*regexp.Regexp{
regexp.MustCompile(`^[^\.\s\/\\]+\.iamcredentials\.googleapis\.com$`),
regexp.MustCompile(`^iamcredentials\.googleapis\.com$`),
regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
}
)

func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
parsed, err := url.Parse(input)
if err != nil {
return false
}
if !strings.EqualFold(parsed.Scheme, scheme) {
return false
}
toTest := parsed.Host

for _, pattern := range patterns {

if valid := pattern.MatchString(toTest); valid {
return true
}
}
return false
}

// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
}

// tokenSource is a private function that's directly called by some of the tests,
// because the unit test URLs are mocked, and would otherwise fail the
// validity check.
func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Regexp, impersonateURLValidPats []*regexp.Regexp, scheme string) (oauth2.TokenSource, error) {
valid := validateURL(c.TokenURL, tokenURLValidPats, scheme)
if !valid {
return nil, fmt.Errorf("oauth2/google: invalid TokenURL provided while constructing tokenSource")
}

if c.ServiceAccountImpersonationURL != "" {
valid := validateURL(c.ServiceAccountImpersonationURL, impersonateURLValidPats, scheme)
if !valid {
return nil, fmt.Errorf("oauth2/google: invalid ServiceAccountImpersonationURL provided while constructing tokenSource")
}
}

ts := tokenSource{
ctx: ctx,
conf: c,
}
if c.ServiceAccountImpersonationURL == "" {
return oauth2.ReuseTokenSource(nil, ts)
return oauth2.ReuseTokenSource(nil, ts), nil
}
scopes := c.Scopes
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
Expand All @@ -68,7 +130,7 @@ func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
scopes: scopes,
ts: oauth2.ReuseTokenSource(nil, ts),
}
return oauth2.ReuseTokenSource(nil, imp)
return oauth2.ReuseTokenSource(nil, imp), nil
}

// Subject token file types.
Expand All @@ -78,9 +140,9 @@ const (
)

type format struct {
// Type is either "text" or "json". When not provided "text" type is assumed.
// Type is either "text" or "json". When not provided "text" type is assumed.
Type string `json:"type"`
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
SubjectTokenFieldName string `json:"subject_token_field_name"`
}

Expand Down Expand Up @@ -128,7 +190,7 @@ type baseCredentialSource interface {
subjectToken() (string, error)
}

// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
type tokenSource struct {
ctx context.Context
conf *Config
Expand Down
115 changes: 115 additions & 0 deletions google/internal/externalaccount/basecredentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
Expand Down Expand Up @@ -95,3 +96,117 @@ func TestToken(t *testing.T) {
}

}

func TestValidateURLTokenURL(t *testing.T) {
var urlValidityTests = []struct {
tokURL string
expectSuccess bool
}{
{"https://east.sts.googleapis.com", true},
{"https://sts.googleapis.com", true},
{"https://sts.asfeasfesef.googleapis.com", true},
{"https://us-east-1-sts.googleapis.com", true},
{"https://sts.googleapis.com/your/path/here", true},
{"https://.sts.googleapis.com", false},
{"https://badsts.googleapis.com", false},
{"https://sts.asfe.asfesef.googleapis.com", false},
{"https://sts..googleapis.com", false},
{"https://-sts.googleapis.com", false},
{"https://us-ea.st-1-sts.googleapis.com", false},
{"https://sts.googleapis.com.evil.com/whatever/path", false},
{"https://us-eas\\t-1.sts.googleapis.com", false},
{"https:/us-ea/st-1.sts.googleapis.com", false},
{"https:/us-east 1.sts.googleapis.com", false},
{"https://", false},
{"http://us-east-1.sts.googleapis.com", false},
{"https://us-east-1.sts.googleapis.comevil.com", false},
}
ctx := context.Background()
for _, tt := range urlValidityTests {
t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
config := testConfig
config.TokenURL = tt.tokURL
_, err := config.TokenSource(ctx)

if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
for _, el := range urlValidityTests {
el.tokURL = strings.ToUpper(el.tokURL)
}
for _, tt := range urlValidityTests {
t.Run(" "+tt.tokURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
config := testConfig
config.TokenURL = tt.tokURL
_, err := config.TokenSource(ctx)

if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
}

func TestValidateURLImpersonateURL(t *testing.T) {
var urlValidityTests = []struct {
impURL string
expectSuccess bool
}{
{"https://east.iamcredentials.googleapis.com", true},
{"https://iamcredentials.googleapis.com", true},
{"https://iamcredentials.asfeasfesef.googleapis.com", true},
{"https://us-east-1-iamcredentials.googleapis.com", true},
{"https://iamcredentials.googleapis.com/your/path/here", true},
{"https://.iamcredentials.googleapis.com", false},
{"https://badiamcredentials.googleapis.com", false},
{"https://iamcredentials.asfe.asfesef.googleapis.com", false},
{"https://iamcredentials..googleapis.com", false},
{"https://-iamcredentials.googleapis.com", false},
{"https://us-ea.st-1-iamcredentials.googleapis.com", false},
{"https://iamcredentials.googleapis.com.evil.com/whatever/path", false},
{"https://us-eas\\t-1.iamcredentials.googleapis.com", false},
{"https:/us-ea/st-1.iamcredentials.googleapis.com", false},
{"https:/us-east 1.iamcredentials.googleapis.com", false},
{"https://", false},
{"http://us-east-1.iamcredentials.googleapis.com", false},
{"https://us-east-1.iamcredentials.googleapis.comevil.com", false},
}
ctx := context.Background()
for _, tt := range urlValidityTests {
t.Run(" "+tt.impURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
config := testConfig
config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
config.ServiceAccountImpersonationURL = tt.impURL
_, err := config.TokenSource(ctx)

if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
for _, el := range urlValidityTests {
el.impURL = strings.ToUpper(el.impURL)
}
for _, tt := range urlValidityTests {
t.Run(" "+tt.impURL, func(t *testing.T) { // We prepend a space ahead of the test input when outputting for sake of readability.
config := testConfig
config.TokenURL = "https://sts.googleapis.com" // Setting the most basic acceptable tokenURL
config.ServiceAccountImpersonationURL = tt.impURL
_, err := config.TokenSource(ctx)

if tt.expectSuccess && err != nil {
t.Errorf("got %v but want nil", err)
} else if !tt.expectSuccess && err == nil {
t.Errorf("got nil but expected an error")
}
})
}
}
3 changes: 2 additions & 1 deletion google/internal/externalaccount/clientauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ package externalaccount

import (
"encoding/base64"
"golang.org/x/oauth2"
"net/http"
"net/url"

"golang.org/x/oauth2"
)

// clientAuthentication represents an OAuth client ID and secret and the mechanism for passing these credentials as stated in rfc6749#2.3.1.
Expand Down
3 changes: 2 additions & 1 deletion google/internal/externalaccount/clientauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
package externalaccount

import (
"golang.org/x/oauth2"
"net/http"
"net/url"
"reflect"
"testing"

"golang.org/x/oauth2"
)

var clientID = "rbrgnognrhongo3bi4gb9ghg9g"
Expand Down
3 changes: 2 additions & 1 deletion google/internal/externalaccount/impersonate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"context"
"encoding/json"
"fmt"
"golang.org/x/oauth2"
"io"
"io/ioutil"
"net/http"
"time"

"golang.org/x/oauth2"
)

// generateAccesstokenReq is used for service account impersonation
Expand Down
7 changes: 6 additions & 1 deletion google/internal/externalaccount/impersonate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"regexp"
"testing"
)

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

testImpersonateConfig.TokenURL = targetServer.URL
ourTS := testImpersonateConfig.TokenSource(context.Background())
allURLs := regexp.MustCompile(".+")
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
if err != nil {
t.Fatalf("Failed to create TokenSource: %v", err)
}

oldNow := now
defer func() { now = oldNow }()
Expand Down
3 changes: 3 additions & 0 deletions google/internal/externalaccount/sts_exchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ func exchangeToken(ctx context.Context, endpoint string, request *stsTokenExchan
defer resp.Body.Close()

body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return nil, err
}
if c := resp.StatusCode; c < 200 || c > 299 {
return nil, fmt.Errorf("oauth2/google: status code %d: %s", c, body)
}
Expand Down
Loading

0 comments on commit 7df4dd6

Please sign in to comment.