forked from remote/oauth2
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec5679f607 | ||
|
|
989acb1bfe | ||
|
|
2323c81c8d | ||
|
|
839de2255f | ||
|
|
0690208dba | ||
|
|
451d5d662f | ||
|
|
cfe200d5bb | ||
|
|
36075149c5 | ||
|
|
4abfd87339 | ||
|
|
1e7f329364 | ||
|
|
86850e0723 | ||
|
|
a6e37e7441 | ||
|
|
54b70c833f | ||
|
|
2fc4ef5a6f | ||
|
|
62b4eedd72 | ||
|
|
885f294722 | ||
|
|
6f9c1a18cc | ||
|
|
c82d0e16dc | ||
|
|
adbaf66a0b | ||
|
|
e07593a4c4 | ||
|
|
34ffb07a99 | ||
|
|
b177c21ac9 | ||
|
|
510acbce1f | ||
|
|
ec4a9b2ff2 | ||
|
|
68a41d64f9 | ||
|
|
1a77549b81 |
12
README.md
12
README.md
@@ -19,7 +19,7 @@ See pkg.go.dev for further documentation and examples.
|
|||||||
* [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2)
|
* [pkg.go.dev/golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2)
|
||||||
* [pkg.go.dev/golang.org/x/oauth2/google](https://pkg.go.dev/golang.org/x/oauth2/google)
|
* [pkg.go.dev/golang.org/x/oauth2/google](https://pkg.go.dev/golang.org/x/oauth2/google)
|
||||||
|
|
||||||
## Policy for new packages
|
## Policy for new endpoints
|
||||||
|
|
||||||
We no longer accept new provider-specific packages in this repo if all
|
We no longer accept new provider-specific packages in this repo if all
|
||||||
they do is add a single endpoint variable. If you just want to add a
|
they do is add a single endpoint variable. If you just want to add a
|
||||||
@@ -29,8 +29,12 @@ package.
|
|||||||
|
|
||||||
## Report Issues / Send Patches
|
## Report Issues / Send Patches
|
||||||
|
|
||||||
This repository uses Gerrit for code changes. To learn how to submit changes to
|
|
||||||
this repository, see https://golang.org/doc/contribute.html.
|
|
||||||
|
|
||||||
The main issue tracker for the oauth2 repository is located at
|
The main issue tracker for the oauth2 repository is located at
|
||||||
https://github.com/golang/oauth2/issues.
|
https://github.com/golang/oauth2/issues.
|
||||||
|
|
||||||
|
This repository uses Gerrit for code changes. To learn how to submit changes to
|
||||||
|
this repository, see https://golang.org/doc/contribute.html. In particular:
|
||||||
|
|
||||||
|
* Excluding trivial changes, all contributions should be connected to an existing issue.
|
||||||
|
* API changes must go through the [change proposal process](https://go.dev/s/proposal-process) before they can be accepted.
|
||||||
|
* The code owners are listed at [dev.golang.org/owners](https://dev.golang.org/owners#:~:text=x/oauth2).
|
||||||
|
|||||||
11
go.mod
11
go.mod
@@ -3,13 +3,14 @@ module golang.org/x/oauth2
|
|||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/compute v1.7.0
|
cloud.google.com/go/compute/metadata v0.2.3
|
||||||
github.com/google/go-cmp v0.5.8
|
github.com/google/go-cmp v0.5.9
|
||||||
golang.org/x/net v0.1.0
|
|
||||||
google.golang.org/appengine v1.6.7
|
google.golang.org/appengine v1.6.7
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/protobuf v1.5.2 // indirect
|
cloud.google.com/go/compute v1.20.1 // indirect
|
||||||
google.golang.org/protobuf v1.28.0 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
golang.org/x/net v0.12.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
"cloud.google.com/go/compute/metadata"
|
"cloud.google.com/go/compute/metadata"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/authhandler"
|
"golang.org/x/oauth2/authhandler"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
|
||||||
|
|
||||||
// Credentials holds Google credentials, including "Application Default Credentials".
|
// Credentials holds Google credentials, including "Application Default Credentials".
|
||||||
// For more details, see:
|
// For more details, see:
|
||||||
// https://developers.google.com/accounts/docs/application-default-credentials
|
// https://developers.google.com/accounts/docs/application-default-credentials
|
||||||
@@ -62,6 +64,18 @@ type CredentialsParams struct {
|
|||||||
|
|
||||||
// PKCE is used to support PKCE flow. Optional for 3LO flow.
|
// PKCE is used to support PKCE flow. Optional for 3LO flow.
|
||||||
PKCE *authhandler.PKCEParams
|
PKCE *authhandler.PKCEParams
|
||||||
|
|
||||||
|
// The OAuth2 TokenURL default override. This value overrides the default TokenURL,
|
||||||
|
// unless explicitly specified by the credentials config file. Optional.
|
||||||
|
TokenURL string
|
||||||
|
|
||||||
|
// EarlyTokenRefresh is the amount of time before a token expires that a new
|
||||||
|
// token will be preemptively fetched. If unset the default value is 10
|
||||||
|
// seconds.
|
||||||
|
//
|
||||||
|
// Note: This option is currently only respected when using credentials
|
||||||
|
// fetched from the GCE metadata server.
|
||||||
|
EarlyTokenRefresh time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func (params CredentialsParams) deepCopy() CredentialsParams {
|
func (params CredentialsParams) deepCopy() CredentialsParams {
|
||||||
@@ -127,17 +141,15 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsPar
|
|||||||
|
|
||||||
// Second, try a well-known file.
|
// Second, try a well-known file.
|
||||||
filename := wellKnownFile()
|
filename := wellKnownFile()
|
||||||
if creds, err := readCredentialsFile(ctx, filename, params); err == nil {
|
if b, err := os.ReadFile(filename); err == nil {
|
||||||
return creds, nil
|
return CredentialsFromJSONWithParams(ctx, b, params)
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("google: error getting credentials using well-known file (%v): %v", filename, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third, if we're on a Google App Engine standard first generation runtime (<= Go 1.9)
|
// Third, if we're on a Google App Engine standard first generation runtime (<= Go 1.9)
|
||||||
// use those credentials. App Engine standard second generation runtimes (>= Go 1.11)
|
// use those credentials. App Engine standard second generation runtimes (>= Go 1.11)
|
||||||
// and App Engine flexible use ComputeTokenSource and the metadata server.
|
// and App Engine flexible use ComputeTokenSource and the metadata server.
|
||||||
if appengineTokenFunc != nil {
|
if appengineTokenFunc != nil {
|
||||||
return &DefaultCredentials{
|
return &Credentials{
|
||||||
ProjectID: appengineAppIDFunc(ctx),
|
ProjectID: appengineAppIDFunc(ctx),
|
||||||
TokenSource: AppEngineTokenSource(ctx, params.Scopes...),
|
TokenSource: AppEngineTokenSource(ctx, params.Scopes...),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -147,15 +159,14 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsPar
|
|||||||
// or App Engine flexible, use the metadata server.
|
// or App Engine flexible, use the metadata server.
|
||||||
if metadata.OnGCE() {
|
if metadata.OnGCE() {
|
||||||
id, _ := metadata.ProjectID()
|
id, _ := metadata.ProjectID()
|
||||||
return &DefaultCredentials{
|
return &Credentials{
|
||||||
ProjectID: id,
|
ProjectID: id,
|
||||||
TokenSource: ComputeTokenSource("", params.Scopes...),
|
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// None are found; return helpful error.
|
// None are found; return helpful error.
|
||||||
const url = "https://developers.google.com/accounts/docs/application-default-credentials"
|
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information", adcSetupURL)
|
||||||
return nil, fmt.Errorf("google: could not find default credentials. See %v for more information.", url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindDefaultCredentials invokes FindDefaultCredentialsWithParams with the specified scopes.
|
// FindDefaultCredentials invokes FindDefaultCredentialsWithParams with the specified scopes.
|
||||||
@@ -194,7 +205,7 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ts = newErrWrappingTokenSource(ts)
|
ts = newErrWrappingTokenSource(ts)
|
||||||
return &DefaultCredentials{
|
return &Credentials{
|
||||||
ProjectID: f.ProjectID,
|
ProjectID: f.ProjectID,
|
||||||
TokenSource: ts,
|
TokenSource: ts,
|
||||||
JSON: jsonData,
|
JSON: jsonData,
|
||||||
@@ -216,8 +227,8 @@ func wellKnownFile() string {
|
|||||||
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f)
|
return filepath.Join(guessUnixHomeDir(), ".config", "gcloud", f)
|
||||||
}
|
}
|
||||||
|
|
||||||
func readCredentialsFile(ctx context.Context, filename string, params CredentialsParams) (*DefaultCredentials, error) {
|
func readCredentialsFile(ctx context.Context, filename string, params CredentialsParams) (*Credentials, error) {
|
||||||
b, err := ioutil.ReadFile(filename)
|
b, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
//
|
//
|
||||||
// Using workload identity federation, your application can access Google Cloud
|
// Using workload identity federation, your application can access Google Cloud
|
||||||
// resources from Amazon Web Services (AWS), Microsoft Azure or any identity
|
// resources from Amazon Web Services (AWS), Microsoft Azure or any identity
|
||||||
// provider that supports OpenID Connect (OIDC).
|
// provider that supports OpenID Connect (OIDC) or SAML 2.0.
|
||||||
// Traditionally, applications running outside Google Cloud have used service
|
// Traditionally, applications running outside Google Cloud have used service
|
||||||
// account keys to access Google Cloud resources. Using identity federation,
|
// account keys to access Google Cloud resources. Using identity federation,
|
||||||
// you can allow your workload to impersonate a service account.
|
// you can allow your workload to impersonate a service account.
|
||||||
@@ -36,26 +36,75 @@
|
|||||||
// Follow the detailed instructions on how to configure Workload Identity Federation
|
// Follow the detailed instructions on how to configure Workload Identity Federation
|
||||||
// in various platforms:
|
// in various platforms:
|
||||||
//
|
//
|
||||||
// Amazon Web Services (AWS): https://cloud.google.com/iam/docs/access-resources-aws
|
// Amazon Web Services (AWS): https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#aws
|
||||||
// Microsoft Azure: https://cloud.google.com/iam/docs/access-resources-azure
|
// Microsoft Azure: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds#azure
|
||||||
// OIDC identity provider: https://cloud.google.com/iam/docs/access-resources-oidc
|
// OIDC identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#oidc
|
||||||
|
// SAML 2.0 identity provider: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml
|
||||||
//
|
//
|
||||||
// For OIDC and SAML providers, the library can retrieve tokens in three ways:
|
// For OIDC and SAML providers, the library can retrieve tokens in three ways:
|
||||||
// from a local file location (file-sourced credentials), from a server
|
// from a local file location (file-sourced credentials), from a server
|
||||||
// (URL-sourced credentials), or from a local executable (executable-sourced
|
// (URL-sourced credentials), or from a local executable (executable-sourced
|
||||||
// credentials).
|
// credentials).
|
||||||
// For file-sourced credentials, a background process needs to be continuously
|
// For file-sourced credentials, a background process needs to be continuously
|
||||||
// refreshing the file location with a new OIDC token prior to expiration.
|
// refreshing the file location with a new OIDC/SAML token prior to expiration.
|
||||||
// For tokens with one hour lifetimes, the token needs to be updated in the file
|
// For tokens with one hour lifetimes, the token needs to be updated in the file
|
||||||
// every hour. The token can be stored directly as plain text or in JSON format.
|
// every hour. The token can be stored directly as plain text or in JSON format.
|
||||||
// For URL-sourced credentials, a local server needs to host a GET endpoint to
|
// For URL-sourced credentials, a local server needs to host a GET endpoint to
|
||||||
// return the OIDC token. The response can be in plain text or JSON.
|
// return the OIDC/SAML token. The response can be in plain text or JSON.
|
||||||
// Additional required request headers can also be specified.
|
// Additional required request headers can also be specified.
|
||||||
// For executable-sourced credentials, an application needs to be available to
|
// For executable-sourced credentials, an application needs to be available to
|
||||||
// output the OIDC token and other information in a JSON format.
|
// output the OIDC/SAML token and other information in a JSON format.
|
||||||
// For more information on how these work (and how to implement
|
// For more information on how these work (and how to implement
|
||||||
// executable-sourced credentials), please check out:
|
// executable-sourced credentials), please check out:
|
||||||
// https://cloud.google.com/iam/docs/using-workload-identity-federation#oidc
|
// https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#create_a_credential_configuration
|
||||||
|
//
|
||||||
|
// Note that this library does not perform any validation on the token_url, token_info_url,
|
||||||
|
// or service_account_impersonation_url fields of the credential configuration.
|
||||||
|
// It is not recommended to use a credential configuration that you did not generate with
|
||||||
|
// the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
|
||||||
|
//
|
||||||
|
// # Workforce Identity Federation
|
||||||
|
//
|
||||||
|
// Workforce identity federation lets you use an external identity provider (IdP) to
|
||||||
|
// authenticate and authorize a workforce—a group of users, such as employees, partners,
|
||||||
|
// and contractors—using IAM, so that the users can access Google Cloud services.
|
||||||
|
// Workforce identity federation extends Google Cloud's identity capabilities to support
|
||||||
|
// syncless, attribute-based single sign on.
|
||||||
|
//
|
||||||
|
// With workforce identity federation, your workforce can access Google Cloud resources
|
||||||
|
// using an external identity provider (IdP) that supports OpenID Connect (OIDC) or
|
||||||
|
// SAML 2.0 such as Azure Active Directory (Azure AD), Active Directory Federation
|
||||||
|
// Services (AD FS), Okta, and others.
|
||||||
|
//
|
||||||
|
// Follow the detailed instructions on how to configure Workload Identity Federation
|
||||||
|
// in various platforms:
|
||||||
|
//
|
||||||
|
// Azure AD: https://cloud.google.com/iam/docs/workforce-sign-in-azure-ad
|
||||||
|
// Okta: https://cloud.google.com/iam/docs/workforce-sign-in-okta
|
||||||
|
// OIDC identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#oidc
|
||||||
|
// SAML 2.0 identity provider: https://cloud.google.com/iam/docs/configuring-workforce-identity-federation#saml
|
||||||
|
//
|
||||||
|
// For workforce identity federation, the library can retrieve tokens in three ways:
|
||||||
|
// from a local file location (file-sourced credentials), from a server
|
||||||
|
// (URL-sourced credentials), or from a local executable (executable-sourced
|
||||||
|
// credentials).
|
||||||
|
// For file-sourced credentials, a background process needs to be continuously
|
||||||
|
// refreshing the file location with a new OIDC/SAML token prior to expiration.
|
||||||
|
// For tokens with one hour lifetimes, the token needs to be updated in the file
|
||||||
|
// every hour. The token can be stored directly as plain text or in JSON format.
|
||||||
|
// For URL-sourced credentials, a local server needs to host a GET endpoint to
|
||||||
|
// return the OIDC/SAML token. The response can be in plain text or JSON.
|
||||||
|
// Additional required request headers can also be specified.
|
||||||
|
// For executable-sourced credentials, an application needs to be available to
|
||||||
|
// output the OIDC/SAML token and other information in a JSON format.
|
||||||
|
// For more information on how these work (and how to implement
|
||||||
|
// executable-sourced credentials), please check out:
|
||||||
|
// https://cloud.google.com/iam/docs/workforce-obtaining-short-lived-credentials#generate_a_configuration_file_for_non-interactive_sign-in
|
||||||
|
//
|
||||||
|
// Note that this library does not perform any validation on the token_url, token_info_url,
|
||||||
|
// or service_account_impersonation_url fields of the credential configuration.
|
||||||
|
// It is not recommended to use a credential configuration that you did not generate with
|
||||||
|
// the gcloud CLI unless you verify that the URL fields point to a googleapis.com domain.
|
||||||
//
|
//
|
||||||
// # Credentials
|
// # Credentials
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ var Endpoint = oauth2.Endpoint{
|
|||||||
AuthStyle: oauth2.AuthStyleInParams,
|
AuthStyle: oauth2.AuthStyleInParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MTLSTokenURL is Google's OAuth 2.0 default mTLS endpoint.
|
||||||
|
const MTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
|
||||||
|
|
||||||
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
|
// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
|
||||||
const JWTTokenURL = "https://oauth2.googleapis.com/token"
|
const JWTTokenURL = "https://oauth2.googleapis.com/token"
|
||||||
|
|
||||||
@@ -172,7 +175,11 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
|
|||||||
cfg.Endpoint.AuthURL = Endpoint.AuthURL
|
cfg.Endpoint.AuthURL = Endpoint.AuthURL
|
||||||
}
|
}
|
||||||
if cfg.Endpoint.TokenURL == "" {
|
if cfg.Endpoint.TokenURL == "" {
|
||||||
cfg.Endpoint.TokenURL = Endpoint.TokenURL
|
if params.TokenURL != "" {
|
||||||
|
cfg.Endpoint.TokenURL = params.TokenURL
|
||||||
|
} else {
|
||||||
|
cfg.Endpoint.TokenURL = Endpoint.TokenURL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tok := &oauth2.Token{RefreshToken: f.RefreshToken}
|
tok := &oauth2.Token{RefreshToken: f.RefreshToken}
|
||||||
return cfg.TokenSource(ctx, tok), nil
|
return cfg.TokenSource(ctx, tok), nil
|
||||||
@@ -224,7 +231,11 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
|
|||||||
// Further information about retrieving access tokens from the GCE metadata
|
// Further information about retrieving access tokens from the GCE metadata
|
||||||
// server can be found at https://cloud.google.com/compute/docs/authentication.
|
// server can be found at https://cloud.google.com/compute/docs/authentication.
|
||||||
func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource {
|
func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource {
|
||||||
return oauth2.ReuseTokenSource(nil, computeSource{account: account, scopes: scope})
|
return computeTokenSource(account, 0, scope...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeTokenSource(account string, earlyExpiry time.Duration, scope ...string) oauth2.TokenSource {
|
||||||
|
return oauth2.ReuseTokenSourceWithExpiry(nil, computeSource{account: account, scopes: scope}, earlyExpiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
type computeSource struct {
|
type computeSource struct {
|
||||||
|
|||||||
@@ -62,6 +62,13 @@ const (
|
|||||||
// The AWS authorization header name for the auto-generated date.
|
// The AWS authorization header name for the auto-generated date.
|
||||||
awsDateHeader = "x-amz-date"
|
awsDateHeader = "x-amz-date"
|
||||||
|
|
||||||
|
// Supported AWS configuration environment variables.
|
||||||
|
awsAccessKeyId = "AWS_ACCESS_KEY_ID"
|
||||||
|
awsDefaultRegion = "AWS_DEFAULT_REGION"
|
||||||
|
awsRegion = "AWS_REGION"
|
||||||
|
awsSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
|
||||||
|
awsSessionToken = "AWS_SESSION_TOKEN"
|
||||||
|
|
||||||
awsTimeFormatLong = "20060102T150405Z"
|
awsTimeFormatLong = "20060102T150405Z"
|
||||||
awsTimeFormatShort = "20060102"
|
awsTimeFormatShort = "20060102"
|
||||||
)
|
)
|
||||||
@@ -267,6 +274,49 @@ type awsRequest struct {
|
|||||||
Headers []awsRequestHeader `json:"headers"`
|
Headers []awsRequestHeader `json:"headers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cs awsCredentialSource) validateMetadataServers() error {
|
||||||
|
if err := cs.validateMetadataServer(cs.RegionURL, "region_url"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cs.validateMetadataServer(cs.CredVerificationURL, "url"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cs.validateMetadataServer(cs.IMDSv2SessionTokenURL, "imdsv2_session_token_url")
|
||||||
|
}
|
||||||
|
|
||||||
|
var validHostnames []string = []string{"169.254.169.254", "fd00:ec2::254"}
|
||||||
|
|
||||||
|
func (cs awsCredentialSource) isValidMetadataServer(metadataUrl string) bool {
|
||||||
|
if metadataUrl == "" {
|
||||||
|
// Zero value means use default, which is valid.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(metadataUrl)
|
||||||
|
if err != nil {
|
||||||
|
// Unparseable URL means invalid
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, validHostname := range validHostnames {
|
||||||
|
if u.Hostname() == validHostname {
|
||||||
|
// If it's one of the valid hostnames, everything is good
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hostname not found in our allowlist, so not valid
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs awsCredentialSource) validateMetadataServer(metadataUrl, urlName string) error {
|
||||||
|
if !cs.isValidMetadataServer(metadataUrl) {
|
||||||
|
return fmt.Errorf("oauth2/google: invalid hostname %s for %s", metadataUrl, urlName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (cs awsCredentialSource) doRequest(req *http.Request) (*http.Response, error) {
|
func (cs awsCredentialSource) doRequest(req *http.Request) (*http.Response, error) {
|
||||||
if cs.client == nil {
|
if cs.client == nil {
|
||||||
cs.client = oauth2.NewClient(cs.ctx, nil)
|
cs.client = oauth2.NewClient(cs.ctx, nil)
|
||||||
@@ -274,16 +324,33 @@ func (cs awsCredentialSource) doRequest(req *http.Request) (*http.Response, erro
|
|||||||
return cs.client.Do(req.WithContext(cs.ctx))
|
return cs.client.Do(req.WithContext(cs.ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canRetrieveRegionFromEnvironment() bool {
|
||||||
|
// The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is
|
||||||
|
// required.
|
||||||
|
return getenv(awsRegion) != "" || getenv(awsDefaultRegion) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func canRetrieveSecurityCredentialFromEnvironment() bool {
|
||||||
|
// Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available.
|
||||||
|
return getenv(awsAccessKeyId) != "" && getenv(awsSecretAccessKey) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldUseMetadataServer() bool {
|
||||||
|
return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()
|
||||||
|
}
|
||||||
|
|
||||||
func (cs awsCredentialSource) subjectToken() (string, error) {
|
func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||||
if cs.requestSigner == nil {
|
if cs.requestSigner == nil {
|
||||||
awsSessionToken, err := cs.getAWSSessionToken()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
if awsSessionToken != "" {
|
if shouldUseMetadataServer() {
|
||||||
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
|
awsSessionToken, err := cs.getAWSSessionToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if awsSessionToken != "" {
|
||||||
|
headers[awsIMDSv2SessionTokenHeader] = awsSessionToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
awsSecurityCredentials, err := cs.getSecurityCredentials(headers)
|
awsSecurityCredentials, err := cs.getSecurityCredentials(headers)
|
||||||
@@ -389,11 +456,11 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) {
|
func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) {
|
||||||
if envAwsRegion := getenv("AWS_REGION"); envAwsRegion != "" {
|
if canRetrieveRegionFromEnvironment() {
|
||||||
return envAwsRegion, nil
|
if envAwsRegion := getenv(awsRegion); envAwsRegion != "" {
|
||||||
}
|
return envAwsRegion, nil
|
||||||
if envAwsRegion := getenv("AWS_DEFAULT_REGION"); envAwsRegion != "" {
|
}
|
||||||
return envAwsRegion, nil
|
return getenv("AWS_DEFAULT_REGION"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cs.RegionURL == "" {
|
if cs.RegionURL == "" {
|
||||||
@@ -434,14 +501,12 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result awsSecurityCredentials, err error) {
|
func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string) (result awsSecurityCredentials, err error) {
|
||||||
if accessKeyID := getenv("AWS_ACCESS_KEY_ID"); accessKeyID != "" {
|
if canRetrieveSecurityCredentialFromEnvironment() {
|
||||||
if secretAccessKey := getenv("AWS_SECRET_ACCESS_KEY"); secretAccessKey != "" {
|
return awsSecurityCredentials{
|
||||||
return awsSecurityCredentials{
|
AccessKeyID: getenv(awsAccessKeyId),
|
||||||
AccessKeyID: accessKeyID,
|
SecretAccessKey: getenv(awsSecretAccessKey),
|
||||||
SecretAccessKey: secretAccessKey,
|
SecurityToken: getenv(awsSessionToken),
|
||||||
SecurityToken: getenv("AWS_SESSION_TOKEN"),
|
}, nil
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
roleName, err := cs.getMetadataRoleName(headers)
|
roleName, err := cs.getMetadataRoleName(headers)
|
||||||
|
|||||||
@@ -474,6 +474,38 @@ func createDefaultAwsTestServer() *testAwsServer {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createDefaultAwsTestServerWithImdsv2(t *testing.T) *testAwsServer {
|
||||||
|
validateSessionTokenHeaders := func(r *http.Request) {
|
||||||
|
if r.URL.Path == "/latest/api/token" {
|
||||||
|
headerValue := r.Header.Get(awsIMDSv2SessionTtlHeader)
|
||||||
|
if headerValue != awsIMDSv2SessionTtl {
|
||||||
|
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTtlHeader, headerValue, awsIMDSv2SessionTtl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headerValue := r.Header.Get(awsIMDSv2SessionTokenHeader)
|
||||||
|
if headerValue != "sessiontoken" {
|
||||||
|
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTokenHeader, headerValue, "sessiontoken")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return createAwsTestServer(
|
||||||
|
"/latest/meta-data/iam/security-credentials",
|
||||||
|
"/latest/meta-data/placement/availability-zone",
|
||||||
|
"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
||||||
|
"/latest/api/token",
|
||||||
|
"gcp-aws-role",
|
||||||
|
"us-east-2b",
|
||||||
|
map[string]string{
|
||||||
|
"SecretAccessKey": secretAccessKey,
|
||||||
|
"AccessKeyId": accessKeyID,
|
||||||
|
"Token": securityToken,
|
||||||
|
},
|
||||||
|
"sessiontoken",
|
||||||
|
validateSessionTokenHeaders,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (server *testAwsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (server *testAwsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
switch p := r.URL.Path; p {
|
switch p := r.URL.Path; p {
|
||||||
case server.url:
|
case server.url:
|
||||||
@@ -553,16 +585,25 @@ func getExpectedSubjectToken(url, region, accessKeyID, secretAccessKey, security
|
|||||||
func TestAWSCredential_BasicRequest(t *testing.T) {
|
func TestAWSCredential_BasicRequest(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
|
||||||
getenv = setEnvironment(map[string]string{})
|
|
||||||
oldNow := now
|
oldNow := now
|
||||||
defer func() { now = oldNow }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
|
getenv = setEnvironment(map[string]string{})
|
||||||
now = setTime(defaultTime)
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -588,46 +629,27 @@ func TestAWSCredential_BasicRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAWSCredential_IMDSv2(t *testing.T) {
|
func TestAWSCredential_IMDSv2(t *testing.T) {
|
||||||
validateSessionTokenHeaders := func(r *http.Request) {
|
server := createDefaultAwsTestServerWithImdsv2(t)
|
||||||
if r.URL.Path == "/latest/api/token" {
|
|
||||||
headerValue := r.Header.Get(awsIMDSv2SessionTtlHeader)
|
|
||||||
if headerValue != awsIMDSv2SessionTtl {
|
|
||||||
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTtlHeader, headerValue, awsIMDSv2SessionTtl)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
headerValue := r.Header.Get(awsIMDSv2SessionTokenHeader)
|
|
||||||
if headerValue != "sessiontoken" {
|
|
||||||
t.Errorf("%q = \n%q\n want \n%q", awsIMDSv2SessionTokenHeader, headerValue, "sessiontoken")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server := createAwsTestServer(
|
|
||||||
"/latest/meta-data/iam/security-credentials",
|
|
||||||
"/latest/meta-data/placement/availability-zone",
|
|
||||||
"https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
|
||||||
"/latest/api/token",
|
|
||||||
"gcp-aws-role",
|
|
||||||
"us-east-2b",
|
|
||||||
map[string]string{
|
|
||||||
"SecretAccessKey": secretAccessKey,
|
|
||||||
"AccessKeyId": accessKeyID,
|
|
||||||
"Token": securityToken,
|
|
||||||
},
|
|
||||||
"sessiontoken",
|
|
||||||
validateSessionTokenHeaders,
|
|
||||||
)
|
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
|
||||||
getenv = setEnvironment(map[string]string{})
|
|
||||||
oldNow := now
|
oldNow := now
|
||||||
defer func() { now = oldNow }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
|
getenv = setEnvironment(map[string]string{})
|
||||||
now = setTime(defaultTime)
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -655,17 +677,26 @@ func TestAWSCredential_IMDSv2(t *testing.T) {
|
|||||||
func TestAWSCredential_BasicRequestWithoutSecurityToken(t *testing.T) {
|
func TestAWSCredential_BasicRequestWithoutSecurityToken(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
delete(server.Credentials, "Token")
|
delete(server.Credentials, "Token")
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
|
||||||
getenv = setEnvironment(map[string]string{})
|
|
||||||
oldNow := now
|
oldNow := now
|
||||||
defer func() { now = oldNow }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
|
getenv = setEnvironment(map[string]string{})
|
||||||
now = setTime(defaultTime)
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -693,20 +724,29 @@ func TestAWSCredential_BasicRequestWithoutSecurityToken(t *testing.T) {
|
|||||||
func TestAWSCredential_BasicRequestWithEnv(t *testing.T) {
|
func TestAWSCredential_BasicRequestWithEnv(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldNow := now
|
||||||
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{
|
getenv = setEnvironment(map[string]string{
|
||||||
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
||||||
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
"AWS_REGION": "us-west-1",
|
"AWS_REGION": "us-west-1",
|
||||||
})
|
})
|
||||||
oldNow := now
|
|
||||||
defer func() { now = oldNow }()
|
|
||||||
now = setTime(defaultTime)
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -734,20 +774,29 @@ func TestAWSCredential_BasicRequestWithEnv(t *testing.T) {
|
|||||||
func TestAWSCredential_BasicRequestWithDefaultEnv(t *testing.T) {
|
func TestAWSCredential_BasicRequestWithDefaultEnv(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldNow := now
|
||||||
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{
|
getenv = setEnvironment(map[string]string{
|
||||||
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
||||||
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
"AWS_DEFAULT_REGION": "us-west-1",
|
"AWS_REGION": "us-west-1",
|
||||||
})
|
})
|
||||||
oldNow := now
|
|
||||||
defer func() { now = oldNow }()
|
|
||||||
now = setTime(defaultTime)
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -774,21 +823,30 @@ func TestAWSCredential_BasicRequestWithDefaultEnv(t *testing.T) {
|
|||||||
func TestAWSCredential_BasicRequestWithTwoRegions(t *testing.T) {
|
func TestAWSCredential_BasicRequestWithTwoRegions(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldNow := now
|
||||||
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{
|
getenv = setEnvironment(map[string]string{
|
||||||
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
||||||
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
"AWS_REGION": "us-west-1",
|
"AWS_REGION": "us-west-1",
|
||||||
"AWS_DEFAULT_REGION": "us-east-1",
|
"AWS_DEFAULT_REGION": "us-east-1",
|
||||||
})
|
})
|
||||||
oldNow := now
|
|
||||||
defer func() { now = oldNow }()
|
|
||||||
now = setTime(defaultTime)
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -815,16 +873,25 @@ func TestAWSCredential_BasicRequestWithTwoRegions(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithBadVersion(t *testing.T) {
|
func TestAWSCredential_RequestWithBadVersion(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
tfc.CredentialSource.EnvironmentID = "aws3"
|
tfc.CredentialSource.EnvironmentID = "aws3"
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
_, err := tfc.parse(context.Background())
|
_, err = tfc.parse(context.Background())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("parse() should have failed")
|
t.Fatalf("parse() should have failed")
|
||||||
}
|
}
|
||||||
@@ -836,14 +903,23 @@ func TestAWSCredential_RequestWithBadVersion(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) {
|
func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
tfc.CredentialSource.RegionURL = ""
|
tfc.CredentialSource.RegionURL = ""
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -863,14 +939,23 @@ func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) {
|
func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
server.WriteRegion = notFound
|
server.WriteRegion = notFound
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -890,6 +975,10 @@ func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
|
func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
server.WriteSecurityCredentials = func(w http.ResponseWriter, r *http.Request) {
|
server.WriteSecurityCredentials = func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("{}"))
|
w.Write([]byte("{}"))
|
||||||
}
|
}
|
||||||
@@ -898,8 +987,13 @@ func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
|
|||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -919,6 +1013,10 @@ func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
|
func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
server.WriteSecurityCredentials = func(w http.ResponseWriter, r *http.Request) {
|
server.WriteSecurityCredentials = func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte(`{"AccessKeyId":"FOOBARBAS"}`))
|
w.Write([]byte(`{"AccessKeyId":"FOOBARBAS"}`))
|
||||||
}
|
}
|
||||||
@@ -927,8 +1025,13 @@ func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
|
|||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -948,14 +1051,23 @@ func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithNoCredentialURL(t *testing.T) {
|
func TestAWSCredential_RequestWithNoCredentialURL(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
tfc.CredentialSource.URL = ""
|
tfc.CredentialSource.URL = ""
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -975,14 +1087,23 @@ func TestAWSCredential_RequestWithNoCredentialURL(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithBadCredentialURL(t *testing.T) {
|
func TestAWSCredential_RequestWithBadCredentialURL(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
server.WriteRolename = notFound
|
server.WriteRolename = notFound
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1002,14 +1123,23 @@ func TestAWSCredential_RequestWithBadCredentialURL(t *testing.T) {
|
|||||||
func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) {
|
func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
server.WriteSecurityCredentials = notFound
|
server.WriteSecurityCredentials = notFound
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
defer func() { getenv = oldGetenv }()
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
getenv = setEnvironment(map[string]string{})
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1025,3 +1155,290 @@ func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) {
|
|||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAWSCredential_ShouldNotCallMetadataEndpointWhenCredsAreInEnv(t *testing.T) {
|
||||||
|
server := createDefaultAwsTestServer()
|
||||||
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataTs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
t.Error("Metadata server should not have been called.")
|
||||||
|
}))
|
||||||
|
|
||||||
|
tfc := testFileConfig
|
||||||
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
tfc.CredentialSource.IMDSv2SessionTokenURL = metadataTs.URL
|
||||||
|
|
||||||
|
oldGetenv := getenv
|
||||||
|
oldNow := now
|
||||||
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
|
getenv = setEnvironment(map[string]string{
|
||||||
|
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
||||||
|
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
|
"AWS_REGION": "us-west-1",
|
||||||
|
})
|
||||||
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
|
base, err := tfc.parse(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse() failed %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := base.subjectToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := getExpectedSubjectToken(
|
||||||
|
"https://sts.us-west-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
||||||
|
"us-west-1",
|
||||||
|
"AKIDEXAMPLE",
|
||||||
|
"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
if got, want := out, expected; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAWSCredential_ShouldCallMetadataEndpointWhenNoRegion(t *testing.T) {
|
||||||
|
server := createDefaultAwsTestServerWithImdsv2(t)
|
||||||
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
|
tfc := testFileConfig
|
||||||
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
|
oldGetenv := getenv
|
||||||
|
oldNow := now
|
||||||
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
|
getenv = setEnvironment(map[string]string{
|
||||||
|
"AWS_ACCESS_KEY_ID": accessKeyID,
|
||||||
|
"AWS_SECRET_ACCESS_KEY": secretAccessKey,
|
||||||
|
})
|
||||||
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
|
base, err := tfc.parse(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse() failed %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := base.subjectToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := getExpectedSubjectToken(
|
||||||
|
"https://sts.us-east-2.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
||||||
|
"us-east-2",
|
||||||
|
accessKeyID,
|
||||||
|
secretAccessKey,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
if got, want := out, expected; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAWSCredential_ShouldCallMetadataEndpointWhenNoAccessKey(t *testing.T) {
|
||||||
|
server := createDefaultAwsTestServerWithImdsv2(t)
|
||||||
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
|
tfc := testFileConfig
|
||||||
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
|
oldGetenv := getenv
|
||||||
|
oldNow := now
|
||||||
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
|
getenv = setEnvironment(map[string]string{
|
||||||
|
"AWS_SECRET_ACCESS_KEY": "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
|
"AWS_REGION": "us-west-1",
|
||||||
|
})
|
||||||
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
|
base, err := tfc.parse(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse() failed %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := base.subjectToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := getExpectedSubjectToken(
|
||||||
|
"https://sts.us-west-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
||||||
|
"us-west-1",
|
||||||
|
accessKeyID,
|
||||||
|
secretAccessKey,
|
||||||
|
securityToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
if got, want := out, expected; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAWSCredential_ShouldCallMetadataEndpointWhenNoSecretAccessKey(t *testing.T) {
|
||||||
|
server := createDefaultAwsTestServerWithImdsv2(t)
|
||||||
|
ts := httptest.NewServer(server)
|
||||||
|
tsURL, err := neturl.Parse(ts.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("couldn't parse httptest servername")
|
||||||
|
}
|
||||||
|
|
||||||
|
tfc := testFileConfig
|
||||||
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
|
oldGetenv := getenv
|
||||||
|
oldNow := now
|
||||||
|
oldValidHostnames := validHostnames
|
||||||
|
defer func() {
|
||||||
|
getenv = oldGetenv
|
||||||
|
now = oldNow
|
||||||
|
validHostnames = oldValidHostnames
|
||||||
|
}()
|
||||||
|
getenv = setEnvironment(map[string]string{
|
||||||
|
"AWS_ACCESS_KEY_ID": "AKIDEXAMPLE",
|
||||||
|
"AWS_REGION": "us-west-1",
|
||||||
|
})
|
||||||
|
now = setTime(defaultTime)
|
||||||
|
validHostnames = []string{tsURL.Hostname()}
|
||||||
|
|
||||||
|
base, err := tfc.parse(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse() failed %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := base.subjectToken()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := getExpectedSubjectToken(
|
||||||
|
"https://sts.us-west-1.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
|
||||||
|
"us-west-1",
|
||||||
|
accessKeyID,
|
||||||
|
secretAccessKey,
|
||||||
|
securityToken,
|
||||||
|
)
|
||||||
|
|
||||||
|
if got, want := out, expected; !reflect.DeepEqual(got, want) {
|
||||||
|
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAWSCredential_Validations(t *testing.T) {
|
||||||
|
var metadataServerValidityTests = []struct {
|
||||||
|
name string
|
||||||
|
credSource CredentialSource
|
||||||
|
errText string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "No Metadata Server URLs",
|
||||||
|
credSource: CredentialSource{
|
||||||
|
EnvironmentID: "aws1",
|
||||||
|
RegionURL: "",
|
||||||
|
URL: "",
|
||||||
|
IMDSv2SessionTokenURL: "",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "IPv4 Metadata Server URLs",
|
||||||
|
credSource: CredentialSource{
|
||||||
|
EnvironmentID: "aws1",
|
||||||
|
RegionURL: "http://169.254.169.254/latest/meta-data/placement/availability-zone",
|
||||||
|
URL: "http://169.254.169.254/latest/meta-data/iam/security-credentials",
|
||||||
|
IMDSv2SessionTokenURL: "http://169.254.169.254/latest/api/token",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "IPv6 Metadata Server URLs",
|
||||||
|
credSource: CredentialSource{
|
||||||
|
EnvironmentID: "aws1",
|
||||||
|
RegionURL: "http://[fd00:ec2::254]/latest/meta-data/placement/availability-zone",
|
||||||
|
URL: "http://[fd00:ec2::254]/latest/meta-data/iam/security-credentials",
|
||||||
|
IMDSv2SessionTokenURL: "http://[fd00:ec2::254]/latest/api/token",
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
name: "Faulty RegionURL",
|
||||||
|
credSource: CredentialSource{
|
||||||
|
EnvironmentID: "aws1",
|
||||||
|
RegionURL: "http://abc.com/latest/meta-data/placement/availability-zone",
|
||||||
|
URL: "http://169.254.169.254/latest/meta-data/iam/security-credentials",
|
||||||
|
IMDSv2SessionTokenURL: "http://169.254.169.254/latest/api/token",
|
||||||
|
},
|
||||||
|
errText: "oauth2/google: invalid hostname http://abc.com/latest/meta-data/placement/availability-zone for region_url",
|
||||||
|
}, {
|
||||||
|
name: "Faulty CredVerificationURL",
|
||||||
|
credSource: CredentialSource{
|
||||||
|
EnvironmentID: "aws1",
|
||||||
|
RegionURL: "http://169.254.169.254/latest/meta-data/placement/availability-zone",
|
||||||
|
URL: "http://abc.com/latest/meta-data/iam/security-credentials",
|
||||||
|
IMDSv2SessionTokenURL: "http://169.254.169.254/latest/api/token",
|
||||||
|
},
|
||||||
|
errText: "oauth2/google: invalid hostname http://abc.com/latest/meta-data/iam/security-credentials for url",
|
||||||
|
}, {
|
||||||
|
name: "Faulty IMDSv2SessionTokenURL",
|
||||||
|
credSource: CredentialSource{
|
||||||
|
EnvironmentID: "aws1",
|
||||||
|
RegionURL: "http://169.254.169.254/latest/meta-data/placement/availability-zone",
|
||||||
|
URL: "http://169.254.169.254/latest/meta-data/iam/security-credentials",
|
||||||
|
IMDSv2SessionTokenURL: "http://abc.com/latest/api/token",
|
||||||
|
},
|
||||||
|
errText: "oauth2/google: invalid hostname http://abc.com/latest/api/token for imdsv2_session_token_url",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range metadataServerValidityTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
tfc := testFileConfig
|
||||||
|
tfc.CredentialSource = tt.credSource
|
||||||
|
|
||||||
|
oldGetenv := getenv
|
||||||
|
defer func() { getenv = oldGetenv }()
|
||||||
|
getenv = setEnvironment(map[string]string{})
|
||||||
|
|
||||||
|
_, err := tfc.parse(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
if tt.errText == "" {
|
||||||
|
t.Errorf("Didn't expect an error, but got %v", err)
|
||||||
|
} else if tt.errText != err.Error() {
|
||||||
|
t.Errorf("Expected %v, but got %v", tt.errText, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if tt.errText != "" {
|
||||||
|
t.Errorf("Expected error %v, but got none", tt.errText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,22 +67,6 @@ type Config struct {
|
|||||||
// that include all elements in a given list, in that order.
|
// that include all elements in a given list, in that order.
|
||||||
|
|
||||||
var (
|
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$`),
|
|
||||||
regexp.MustCompile(`(?i)^sts-[^\.\s\/\\]+\.p\.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$`),
|
|
||||||
regexp.MustCompile(`^iamcredentials-[^\.\s\/\\]+\.p\.googleapis\.com$`),
|
|
||||||
}
|
|
||||||
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,25 +94,13 @@ func validateWorkforceAudience(input string) bool {
|
|||||||
|
|
||||||
// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
|
// 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, error) {
|
func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
|
||||||
return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
|
return c.tokenSource(ctx, "https")
|
||||||
}
|
}
|
||||||
|
|
||||||
// tokenSource is a private function that's directly called by some of the tests,
|
// 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
|
// because the unit test URLs are mocked, and would otherwise fail the
|
||||||
// validity check.
|
// validity check.
|
||||||
func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Regexp, impersonateURLValidPats []*regexp.Regexp, scheme string) (oauth2.TokenSource, error) {
|
func (c *Config) tokenSource(ctx context.Context, 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.WorkforcePoolUserProject != "" {
|
if c.WorkforcePoolUserProject != "" {
|
||||||
valid := validateWorkforceAudience(c.Audience)
|
valid := validateWorkforceAudience(c.Audience)
|
||||||
if !valid {
|
if !valid {
|
||||||
@@ -213,6 +185,10 @@ func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
|||||||
awsCredSource.IMDSv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
awsCredSource.IMDSv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := awsCredSource.validateMetadataServers(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return awsCredSource, nil
|
return awsCredSource, nil
|
||||||
}
|
}
|
||||||
} else if c.CredentialSource.File != "" {
|
} else if c.CredentialSource.File != "" {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -208,140 +207,6 @@ func TestNonworkforceWithWorkforcePoolUserProject(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},
|
|
||||||
{"https://sts-xyz.p.googleapis.com", true},
|
|
||||||
{"https://sts.pgoogleapis.com", false},
|
|
||||||
{"https://p.googleapis.com", false},
|
|
||||||
{"https://sts.p.com", false},
|
|
||||||
{"http://sts.p.googleapis.com", false},
|
|
||||||
{"https://xyz-sts.p.googleapis.com", false},
|
|
||||||
{"https://sts-xyz.123.p.googleapis.com", false},
|
|
||||||
{"https://sts-xyz.p1.googleapis.com", false},
|
|
||||||
{"https://sts-xyz.p.foo.com", false},
|
|
||||||
{"https://sts-xyz.p.foo.googleapis.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},
|
|
||||||
{"https://iamcredentials-xyz.p.googleapis.com", true},
|
|
||||||
{"https://iamcredentials.pgoogleapis.com", false},
|
|
||||||
{"https://p.googleapis.com", false},
|
|
||||||
{"https://iamcredentials.p.com", false},
|
|
||||||
{"http://iamcredentials.p.googleapis.com", false},
|
|
||||||
{"https://xyz-iamcredentials.p.googleapis.com", false},
|
|
||||||
{"https://iamcredentials-xyz.123.p.googleapis.com", false},
|
|
||||||
{"https://iamcredentials-xyz.p1.googleapis.com", false},
|
|
||||||
{"https://iamcredentials-xyz.p.foo.com", false},
|
|
||||||
{"https://iamcredentials-xyz.p.foo.googleapis.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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkforcePoolCreation(t *testing.T) {
|
func TestWorkforcePoolCreation(t *testing.T) {
|
||||||
var audienceValidatyTests = []struct {
|
var audienceValidatyTests = []struct {
|
||||||
audience string
|
audience string
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"regexp"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -114,8 +113,7 @@ func TestImpersonation(t *testing.T) {
|
|||||||
defer targetServer.Close()
|
defer targetServer.Close()
|
||||||
testImpersonateConfig.TokenURL = targetServer.URL
|
testImpersonateConfig.TokenURL = targetServer.URL
|
||||||
|
|
||||||
allURLs := regexp.MustCompile(".+")
|
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http")
|
||||||
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), []*regexp.Regexp{allURLs}, []*regexp.Regexp{allURLs}, "http")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create TokenSource: %v", err)
|
t.Fatalf("Failed to create TokenSource: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
// ParseKey converts the binary contents of a private key file
|
// ParseKey converts the binary contents of a private key file
|
||||||
// to an *rsa.PrivateKey. It detects whether the private key is in a
|
// to an *rsa.PrivateKey. It detects whether the private key is in a
|
||||||
// PEM container or not. If so, it extracts the the private key
|
// PEM container or not. If so, it extracts the private key
|
||||||
// from PEM container before conversion. It only supports PEM
|
// from PEM container before conversion. It only supports PEM
|
||||||
// containers with no passphrase.
|
// containers with no passphrase.
|
||||||
func ParseKey(key []byte) (*rsa.PrivateKey, error) {
|
func ParseKey(key []byte) (*rsa.PrivateKey, error) {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"golang.org/x/net/context/ctxhttp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Token represents the credentials used to authorize
|
// Token represents the credentials used to authorize
|
||||||
@@ -57,12 +55,18 @@ type Token struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tokenJSON is the struct representing the HTTP response from OAuth2
|
// tokenJSON is the struct representing the HTTP response from OAuth2
|
||||||
// providers returning a token in JSON form.
|
// providers returning a token or error in JSON form.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
|
||||||
type tokenJSON struct {
|
type tokenJSON struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
|
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
|
||||||
|
// error fields
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||||
|
ErrorCode string `json:"error"`
|
||||||
|
ErrorDescription string `json:"error_description"`
|
||||||
|
ErrorURI string `json:"error_uri"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *tokenJSON) expiry() (t time.Time) {
|
func (e *tokenJSON) expiry() (t time.Time) {
|
||||||
@@ -229,7 +233,7 @@ func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
|
func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
|
||||||
r, err := ctxhttp.Do(ctx, ContextClient(ctx), req)
|
r, err := ContextClient(ctx).Do(req.WithContext(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -238,21 +242,29 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
|
||||||
}
|
}
|
||||||
if code := r.StatusCode; code < 200 || code > 299 {
|
|
||||||
return nil, &RetrieveError{
|
failureStatus := r.StatusCode < 200 || r.StatusCode > 299
|
||||||
Response: r,
|
retrieveError := &RetrieveError{
|
||||||
Body: body,
|
Response: r,
|
||||||
}
|
Body: body,
|
||||||
|
// attempt to populate error detail below
|
||||||
}
|
}
|
||||||
|
|
||||||
var token *Token
|
var token *Token
|
||||||
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
|
||||||
switch content {
|
switch content {
|
||||||
case "application/x-www-form-urlencoded", "text/plain":
|
case "application/x-www-form-urlencoded", "text/plain":
|
||||||
|
// some endpoints return a query string
|
||||||
vals, err := url.ParseQuery(string(body))
|
vals, err := url.ParseQuery(string(body))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
if failureStatus {
|
||||||
|
return nil, retrieveError
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("oauth2: cannot parse response: %v", err)
|
||||||
}
|
}
|
||||||
|
retrieveError.ErrorCode = vals.Get("error")
|
||||||
|
retrieveError.ErrorDescription = vals.Get("error_description")
|
||||||
|
retrieveError.ErrorURI = vals.Get("error_uri")
|
||||||
token = &Token{
|
token = &Token{
|
||||||
AccessToken: vals.Get("access_token"),
|
AccessToken: vals.Get("access_token"),
|
||||||
TokenType: vals.Get("token_type"),
|
TokenType: vals.Get("token_type"),
|
||||||
@@ -267,8 +279,14 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
|
|||||||
default:
|
default:
|
||||||
var tj tokenJSON
|
var tj tokenJSON
|
||||||
if err = json.Unmarshal(body, &tj); err != nil {
|
if err = json.Unmarshal(body, &tj); err != nil {
|
||||||
return nil, err
|
if failureStatus {
|
||||||
|
return nil, retrieveError
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("oauth2: cannot parse json: %v", err)
|
||||||
}
|
}
|
||||||
|
retrieveError.ErrorCode = tj.ErrorCode
|
||||||
|
retrieveError.ErrorDescription = tj.ErrorDescription
|
||||||
|
retrieveError.ErrorURI = tj.ErrorURI
|
||||||
token = &Token{
|
token = &Token{
|
||||||
AccessToken: tj.AccessToken,
|
AccessToken: tj.AccessToken,
|
||||||
TokenType: tj.TokenType,
|
TokenType: tj.TokenType,
|
||||||
@@ -278,17 +296,37 @@ func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
|
|||||||
}
|
}
|
||||||
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
|
json.Unmarshal(body, &token.Raw) // no error checks for optional fields
|
||||||
}
|
}
|
||||||
|
// according to spec, servers should respond status 400 in error case
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
|
||||||
|
// but some unorthodox servers respond 200 in error case
|
||||||
|
if failureStatus || retrieveError.ErrorCode != "" {
|
||||||
|
return nil, retrieveError
|
||||||
|
}
|
||||||
if token.AccessToken == "" {
|
if token.AccessToken == "" {
|
||||||
return nil, errors.New("oauth2: server response missing access_token")
|
return nil, errors.New("oauth2: server response missing access_token")
|
||||||
}
|
}
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mirrors oauth2.RetrieveError
|
||||||
type RetrieveError struct {
|
type RetrieveError struct {
|
||||||
Response *http.Response
|
Response *http.Response
|
||||||
Body []byte
|
Body []byte
|
||||||
|
ErrorCode string
|
||||||
|
ErrorDescription string
|
||||||
|
ErrorURI string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RetrieveError) Error() string {
|
func (r *RetrieveError) Error() string {
|
||||||
|
if r.ErrorCode != "" {
|
||||||
|
s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
|
||||||
|
if r.ErrorDescription != "" {
|
||||||
|
s += fmt.Sprintf(" %q", r.ErrorDescription)
|
||||||
|
}
|
||||||
|
if r.ErrorURI != "" {
|
||||||
|
s += fmt.Sprintf(" %q", r.ErrorURI)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
|
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
|
||||||
}
|
}
|
||||||
|
|||||||
33
oauth2.go
33
oauth2.go
@@ -16,6 +16,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"golang.org/x/oauth2/internal"
|
"golang.org/x/oauth2/internal"
|
||||||
)
|
)
|
||||||
@@ -140,7 +141,7 @@ func SetAuthURLParam(key, value string) AuthCodeOption {
|
|||||||
//
|
//
|
||||||
// State is a token to protect the user from CSRF attacks. You must
|
// State is a token to protect the user from CSRF attacks. You must
|
||||||
// always provide a non-empty string and validate that it matches the
|
// always provide a non-empty string and validate that it matches the
|
||||||
// the state query parameter on your redirect callback.
|
// state query parameter on your redirect callback.
|
||||||
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
// See http://tools.ietf.org/html/rfc6749#section-10.12 for more info.
|
||||||
//
|
//
|
||||||
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
|
// Opts may include AccessTypeOnline or AccessTypeOffline, as well
|
||||||
@@ -290,6 +291,8 @@ type reuseTokenSource struct {
|
|||||||
|
|
||||||
mu sync.Mutex // guards t
|
mu sync.Mutex // guards t
|
||||||
t *Token
|
t *Token
|
||||||
|
|
||||||
|
expiryDelta time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Token returns the current token if it's still valid, else will
|
// Token returns the current token if it's still valid, else will
|
||||||
@@ -305,6 +308,7 @@ func (s *reuseTokenSource) Token() (*Token, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
t.expiryDelta = s.expiryDelta
|
||||||
s.t = t
|
s.t = t
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
@@ -379,3 +383,30 @@ func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
|
|||||||
new: src,
|
new: src,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReuseTokenSource returns a TokenSource that acts in the same manner as the
|
||||||
|
// TokenSource returned by ReuseTokenSource, except the expiry buffer is
|
||||||
|
// configurable. The expiration time of a token is calculated as
|
||||||
|
// t.Expiry.Add(-earlyExpiry).
|
||||||
|
func ReuseTokenSourceWithExpiry(t *Token, src TokenSource, earlyExpiry time.Duration) TokenSource {
|
||||||
|
// Don't wrap a reuseTokenSource in itself. That would work,
|
||||||
|
// but cause an unnecessary number of mutex operations.
|
||||||
|
// Just build the equivalent one.
|
||||||
|
if rt, ok := src.(*reuseTokenSource); ok {
|
||||||
|
if t == nil {
|
||||||
|
// Just use it directly, but set the expiryDelta to earlyExpiry,
|
||||||
|
// so the behavior matches what the user expects.
|
||||||
|
rt.expiryDelta = earlyExpiry
|
||||||
|
return rt
|
||||||
|
}
|
||||||
|
src = rt.new
|
||||||
|
}
|
||||||
|
if t != nil {
|
||||||
|
t.expiryDelta = earlyExpiry
|
||||||
|
}
|
||||||
|
return &reuseTokenSource{
|
||||||
|
t: t,
|
||||||
|
new: src,
|
||||||
|
expiryDelta: earlyExpiry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -484,6 +484,7 @@ func TestTokenRetrieveError(t *testing.T) {
|
|||||||
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-type", "application/json")
|
w.Header().Set("Content-type", "application/json")
|
||||||
|
// "The authorization server responds with an HTTP 400 (Bad Request)" https://www.rfc-editor.org/rfc/rfc6749#section-5.2
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
w.Write([]byte(`{"error": "invalid_grant"}`))
|
w.Write([]byte(`{"error": "invalid_grant"}`))
|
||||||
}))
|
}))
|
||||||
@@ -493,15 +494,47 @@ func TestTokenRetrieveError(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("got no error, expected one")
|
t.Fatalf("got no error, expected one")
|
||||||
}
|
}
|
||||||
_, ok := err.(*RetrieveError)
|
re, ok := err.(*RetrieveError)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
|
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
|
||||||
}
|
}
|
||||||
// Test error string for backwards compatibility
|
expected := `oauth2: "invalid_grant"`
|
||||||
expected := fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", "400 Bad Request", `{"error": "invalid_grant"}`)
|
|
||||||
if errStr := err.Error(); errStr != expected {
|
if errStr := err.Error(); errStr != expected {
|
||||||
t.Fatalf("got %#v, expected %#v", errStr, expected)
|
t.Fatalf("got %#v, expected %#v", errStr, expected)
|
||||||
}
|
}
|
||||||
|
expected = "invalid_grant"
|
||||||
|
if re.ErrorCode != expected {
|
||||||
|
t.Fatalf("got %#v, expected %#v", re.ErrorCode, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestTokenRetrieveError200 tests handling of unorthodox server that returns 200 in error case
|
||||||
|
func TestTokenRetrieveError200(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.String() != "/token" {
|
||||||
|
t.Errorf("Unexpected token refresh request URL, %v is found.", r.URL)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-type", "application/json")
|
||||||
|
w.Write([]byte(`{"error": "invalid_grant"}`))
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
conf := newConf(ts.URL)
|
||||||
|
_, err := conf.Exchange(context.Background(), "exchange-code")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("got no error, expected one")
|
||||||
|
}
|
||||||
|
re, ok := err.(*RetrieveError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("got %T error, expected *RetrieveError; error was: %v", err, err)
|
||||||
|
}
|
||||||
|
expected := `oauth2: "invalid_grant"`
|
||||||
|
if errStr := err.Error(); errStr != expected {
|
||||||
|
t.Fatalf("got %#v, expected %#v", errStr, expected)
|
||||||
|
}
|
||||||
|
expected = "invalid_grant"
|
||||||
|
if re.ErrorCode != expected {
|
||||||
|
t.Fatalf("got %#v, expected %#v", re.ErrorCode, expected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {
|
func TestRefreshToken_RefreshTokenReplacement(t *testing.T) {
|
||||||
|
|||||||
33
token.go
33
token.go
@@ -16,10 +16,10 @@ import (
|
|||||||
"golang.org/x/oauth2/internal"
|
"golang.org/x/oauth2/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
// expiryDelta determines how earlier a token should be considered
|
// defaultExpiryDelta determines how earlier a token should be considered
|
||||||
// expired than its actual expiration time. It is used to avoid late
|
// expired than its actual expiration time. It is used to avoid late
|
||||||
// expirations due to client-server time mismatches.
|
// expirations due to client-server time mismatches.
|
||||||
const expiryDelta = 10 * time.Second
|
const defaultExpiryDelta = 10 * time.Second
|
||||||
|
|
||||||
// Token represents the credentials used to authorize
|
// Token represents the credentials used to authorize
|
||||||
// the requests to access protected resources on the OAuth 2.0
|
// the requests to access protected resources on the OAuth 2.0
|
||||||
@@ -52,6 +52,11 @@ type Token struct {
|
|||||||
// raw optionally contains extra metadata from the server
|
// raw optionally contains extra metadata from the server
|
||||||
// when updating a token.
|
// when updating a token.
|
||||||
raw interface{}
|
raw interface{}
|
||||||
|
|
||||||
|
// expiryDelta is used to calculate when a token is considered
|
||||||
|
// expired, by subtracting from Expiry. If zero, defaultExpiryDelta
|
||||||
|
// is used.
|
||||||
|
expiryDelta time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type returns t.TokenType if non-empty, else "Bearer".
|
// Type returns t.TokenType if non-empty, else "Bearer".
|
||||||
@@ -127,6 +132,11 @@ func (t *Token) expired() bool {
|
|||||||
if t.Expiry.IsZero() {
|
if t.Expiry.IsZero() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expiryDelta := defaultExpiryDelta
|
||||||
|
if t.expiryDelta != 0 {
|
||||||
|
expiryDelta = t.expiryDelta
|
||||||
|
}
|
||||||
return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow())
|
return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,14 +175,31 @@ func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RetrieveError is the error returned when the token endpoint returns a
|
// RetrieveError is the error returned when the token endpoint returns a
|
||||||
// non-2XX HTTP status code.
|
// non-2XX HTTP status code or populates RFC 6749's 'error' parameter.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
||||||
type RetrieveError struct {
|
type RetrieveError struct {
|
||||||
Response *http.Response
|
Response *http.Response
|
||||||
// Body is the body that was consumed by reading Response.Body.
|
// Body is the body that was consumed by reading Response.Body.
|
||||||
// It may be truncated.
|
// It may be truncated.
|
||||||
Body []byte
|
Body []byte
|
||||||
|
// ErrorCode is RFC 6749's 'error' parameter.
|
||||||
|
ErrorCode string
|
||||||
|
// ErrorDescription is RFC 6749's 'error_description' parameter.
|
||||||
|
ErrorDescription string
|
||||||
|
// ErrorURI is RFC 6749's 'error_uri' parameter.
|
||||||
|
ErrorURI string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RetrieveError) Error() string {
|
func (r *RetrieveError) Error() string {
|
||||||
|
if r.ErrorCode != "" {
|
||||||
|
s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
|
||||||
|
if r.ErrorDescription != "" {
|
||||||
|
s += fmt.Sprintf(" %q", r.ErrorDescription)
|
||||||
|
}
|
||||||
|
if r.ErrorURI != "" {
|
||||||
|
s += fmt.Sprintf(" %q", r.ErrorURI)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
|
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,13 @@ func TestTokenExpiry(t *testing.T) {
|
|||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false},
|
{name: "12 seconds", tok: &Token{Expiry: now.Add(12 * time.Second)}, want: false},
|
||||||
{name: "10 seconds", tok: &Token{Expiry: now.Add(expiryDelta)}, want: false},
|
{name: "10 seconds", tok: &Token{Expiry: now.Add(defaultExpiryDelta)}, want: false},
|
||||||
{name: "10 seconds-1ns", tok: &Token{Expiry: now.Add(expiryDelta - 1*time.Nanosecond)}, want: true},
|
{name: "10 seconds-1ns", tok: &Token{Expiry: now.Add(defaultExpiryDelta - 1*time.Nanosecond)}, want: true},
|
||||||
{name: "-1 hour", tok: &Token{Expiry: now.Add(-1 * time.Hour)}, want: true},
|
{name: "-1 hour", tok: &Token{Expiry: now.Add(-1 * time.Hour)}, want: true},
|
||||||
|
{name: "12 seconds, custom expiryDelta", tok: &Token{Expiry: now.Add(12 * time.Second), expiryDelta: time.Second * 5}, want: false},
|
||||||
|
{name: "5 seconds, custom expiryDelta", tok: &Token{Expiry: now.Add(time.Second * 5), expiryDelta: time.Second * 5}, want: false},
|
||||||
|
{name: "5 seconds-1ns, custom expiryDelta", tok: &Token{Expiry: now.Add(time.Second*5 - 1*time.Nanosecond), expiryDelta: time.Second * 5}, want: true},
|
||||||
|
{name: "-1 hour, custom expiryDelta", tok: &Token{Expiry: now.Add(-1 * time.Hour), expiryDelta: time.Second * 5}, want: true},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
if got, want := tc.tok.expired(), tc.want; got != want {
|
if got, want := tc.tok.expired(), tc.want; got != want {
|
||||||
|
|||||||
Reference in New Issue
Block a user