Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
a8e26b9
oauth2/google: implement support for token downscoping to allow for r…
gIthuriel Jun 9, 2021
52684dc
First set of comment changes.
gIthuriel Jun 9, 2021
eb57311
Add some validity testing for AccessBoundaryRules and add documentation.
gIthuriel Jun 10, 2021
add9801
Add exmaple showing how NewTokenSource should be called.
gIthuriel Jun 10, 2021
e035bf9
go fmt
gIthuriel Jun 10, 2021
69736ff
downscope: make changes related to comments, including adding another…
gIthuriel Jun 11, 2021
be467ee
downscope: update comment formatting
gIthuriel Jun 11, 2021
c4c64d5
downscope: add some context to returned error
gIthuriel Jun 14, 2021
776a9ed
downscope: move example files to a separate file & package
gIthuriel Jun 14, 2021
b594a60
downscope: minor tweaks
gIthuriel Jun 16, 2021
cbbc506
downscope: fixing nits and renaming
gIthuriel Jun 17, 2021
1d9ea0c
downscope: refactor main functionality into a method on a tokenSource…
gIthuriel Jun 17, 2021
a362f28
downscope: fix grammar and punctuation.
gIthuriel Jun 17, 2021
304d28b
downscope: further updates and nits
gIthuriel Jun 22, 2021
1024258
downscope: refactor some code to remove an extraneous function and in…
gIthuriel Jun 22, 2021
1888dba
downscope: change return type of NewTokenSource
gIthuriel Jun 23, 2021
fec7137
downscope: fix some nits
gIthuriel Jun 24, 2021
941cf10
downscope: move validation checks
gIthuriel Jun 24, 2021
c976479
downscope: update documentation
gIthuriel Jul 29, 2021
d921d8f
Merge pull request #1 from Galadros/downscopeDocumentation
Galadros Jul 29, 2021
e4ec8cd
Removed some code that's not yet finished
gIthuriel Jul 29, 2021
3045b9f
Merge branch 'master' into master
Galadros Jul 29, 2021
0bd54f5
downscope: documentation tweaks
gIthuriel Jul 31, 2021
e4caaa9
Merge branch 'master' of github.com:Galadros/oauth2
gIthuriel Jul 31, 2021
63894e5
Update example_test.go
Galadros Aug 4, 2021
387bb65
Merge branch 'golang:master' into master
Galadros Aug 5, 2021
0925f5e
google/externalaccount: validate tokenURL and ServiceAccountImpersona…
gIthuriel Aug 6, 2021
57c99ca
Rearranged tests for clarity and added one additional positive test.
gIthuriel Aug 6, 2021
1092922
made some changes
gIthuriel Aug 9, 2021
55a616b
properly modified google.go to fix error
gIthuriel Aug 10, 2021
844e38f
tweak regex filters
gIthuriel Aug 10, 2021
280ee39
filter URL to exclude path, update regex accordingly
gIthuriel Aug 11, 2021
a55ea9e
update regex to check url scheme seprately
gIthuriel Aug 12, 2021
98cc3c1
regexes ignore case. Update tests.
gIthuriel Aug 12, 2021
b46ea24
removed commented code
gIthuriel Aug 12, 2021
e8d4c9f
fix nits
gIthuriel Aug 12, 2021
ddf4dbd
check error that was unchecked
gIthuriel Aug 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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