forked from remote/oauth2
Compare commits
1 Commits
master
...
452d0a63bf
| Author | SHA1 | Date | |
|---|---|---|---|
| 452d0a63bf |
@@ -22,7 +22,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
|
adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
|
||||||
defaultUniverseDomain = "googleapis.com"
|
universeDomainDefault = "googleapis.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Credentials holds Google credentials, including "Application Default Credentials".
|
// Credentials holds Google credentials, including "Application Default Credentials".
|
||||||
@@ -42,17 +42,6 @@ type Credentials struct {
|
|||||||
// running on Google Cloud Platform.
|
// running on Google Cloud Platform.
|
||||||
JSON []byte
|
JSON []byte
|
||||||
|
|
||||||
// UniverseDomainProvider returns the default service domain for a given
|
|
||||||
// Cloud universe. Optional.
|
|
||||||
//
|
|
||||||
// On GCE, UniverseDomainProvider should return the universe domain value
|
|
||||||
// from Google Compute Engine (GCE)'s metadata server. See also [The attached service
|
|
||||||
// account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
|
|
||||||
// If the GCE metadata server returns a 404 error, the default universe
|
|
||||||
// domain value should be returned. If the GCE metadata server returns an
|
|
||||||
// error other than 404, the error should be returned.
|
|
||||||
UniverseDomainProvider func() (string, error)
|
|
||||||
|
|
||||||
udMu sync.Mutex // guards universeDomain
|
udMu sync.Mutex // guards universeDomain
|
||||||
// universeDomain is the default service domain for a given Cloud universe.
|
// universeDomain is the default service domain for a given Cloud universe.
|
||||||
universeDomain string
|
universeDomain string
|
||||||
@@ -69,38 +58,60 @@ type Credentials struct {
|
|||||||
// See also [The attached service account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
|
// See also [The attached service account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
|
||||||
func (c *Credentials) UniverseDomain() string {
|
func (c *Credentials) UniverseDomain() string {
|
||||||
if c.universeDomain == "" {
|
if c.universeDomain == "" {
|
||||||
return defaultUniverseDomain
|
return universeDomainDefault
|
||||||
}
|
}
|
||||||
return c.universeDomain
|
return c.universeDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUniverseDomain returns the default service domain for a given Cloud
|
// GetUniverseDomain returns the default service domain for a given Cloud
|
||||||
// universe. If present, UniverseDomainProvider will be invoked and its return
|
// universe.
|
||||||
// value will be cached.
|
|
||||||
//
|
//
|
||||||
// The default value is "googleapis.com".
|
// The default value is "googleapis.com".
|
||||||
|
//
|
||||||
|
// It obtains the universe domain from the attached service account on GCE when
|
||||||
|
// authenticating via the GCE metadata server. See also [The attached service
|
||||||
|
// account](https://cloud.google.com/docs/authentication/application-default-credentials#attached-sa).
|
||||||
|
// If the GCE metadata server returns a 404 error, the default value is
|
||||||
|
// returned. If the GCE metadata server returns an error other than 404, the
|
||||||
|
// error is returned.
|
||||||
func (c *Credentials) GetUniverseDomain() (string, error) {
|
func (c *Credentials) GetUniverseDomain() (string, error) {
|
||||||
c.udMu.Lock()
|
c.udMu.Lock()
|
||||||
defer c.udMu.Unlock()
|
defer c.udMu.Unlock()
|
||||||
if c.universeDomain == "" && c.UniverseDomainProvider != nil {
|
if c.universeDomain == "" && metadata.OnGCE() {
|
||||||
// On Google Compute Engine, an App Engine standard second generation
|
// If we're on Google Compute Engine, an App Engine standard second
|
||||||
// runtime, or App Engine flexible, use an externally provided function
|
// generation runtime, or App Engine flexible, use the metadata server.
|
||||||
// to request the universe domain from the metadata server.
|
err := c.computeUniverseDomain()
|
||||||
ud, err := c.UniverseDomainProvider()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
c.universeDomain = ud
|
|
||||||
}
|
}
|
||||||
// If no UniverseDomainProvider (meaning not on Google Compute Engine), or
|
// If not on Google Compute Engine, or in case of any non-error path in
|
||||||
// in case of any (non-error) empty return value from
|
// computeUniverseDomain that did not set universeDomain, set the default
|
||||||
// UniverseDomainProvider, set the default universe domain.
|
// universe domain.
|
||||||
if c.universeDomain == "" {
|
if c.universeDomain == "" {
|
||||||
c.universeDomain = defaultUniverseDomain
|
c.universeDomain = universeDomainDefault
|
||||||
}
|
}
|
||||||
return c.universeDomain, nil
|
return c.universeDomain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// computeUniverseDomain fetches the default service domain for a given Cloud
|
||||||
|
// universe from Google Compute Engine (GCE)'s metadata server. It's only valid
|
||||||
|
// to use this method if your program is running on a GCE instance.
|
||||||
|
func (c *Credentials) computeUniverseDomain() error {
|
||||||
|
var err error
|
||||||
|
c.universeDomain, err = metadata.Get("universe/universe_domain")
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(metadata.NotDefinedError); ok {
|
||||||
|
// http.StatusNotFound (404)
|
||||||
|
c.universeDomain = universeDomainDefault
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultCredentials is the old name of Credentials.
|
// DefaultCredentials is the old name of Credentials.
|
||||||
//
|
//
|
||||||
// Deprecated: use Credentials instead.
|
// Deprecated: use Credentials instead.
|
||||||
@@ -188,7 +199,9 @@ func DefaultTokenSource(ctx context.Context, scope ...string) (oauth2.TokenSourc
|
|||||||
// 2. A JSON file in a location known to the gcloud command-line tool.
|
// 2. A JSON file in a location known to the gcloud command-line tool.
|
||||||
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
|
// On Windows, this is %APPDATA%/gcloud/application_default_credentials.json.
|
||||||
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
// On other systems, $HOME/.config/gcloud/application_default_credentials.json.
|
||||||
// 3. On Google Compute Engine, Google App Engine standard second generation runtimes
|
// 3. On Google App Engine standard first generation runtimes (<= Go 1.9) it uses
|
||||||
|
// the appengine.AccessToken function.
|
||||||
|
// 4. On Google Compute Engine, Google App Engine standard second generation runtimes
|
||||||
// (>= Go 1.11), and Google App Engine flexible environment, it fetches
|
// (>= Go 1.11), and Google App Engine flexible environment, it fetches
|
||||||
// credentials from the metadata server.
|
// credentials from the metadata server.
|
||||||
func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) {
|
func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsParams) (*Credentials, error) {
|
||||||
@@ -211,26 +224,13 @@ func FindDefaultCredentialsWithParams(ctx context.Context, params CredentialsPar
|
|||||||
return CredentialsFromJSONWithParams(ctx, b, params)
|
return CredentialsFromJSONWithParams(ctx, b, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third, if we're on Google Compute Engine, an App Engine standard second generation runtime,
|
// Fourth, if we're on Google Compute Engine, an App Engine standard second generation runtime,
|
||||||
// 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()
|
||||||
universeDomainProvider := func() (string, error) {
|
|
||||||
universeDomain, err := metadata.Get("universe/universe_domain")
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(metadata.NotDefinedError); ok {
|
|
||||||
// http.StatusNotFound (404)
|
|
||||||
return defaultUniverseDomain, nil
|
|
||||||
} else {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return universeDomain, nil
|
|
||||||
}
|
|
||||||
return &Credentials{
|
return &Credentials{
|
||||||
ProjectID: id,
|
ProjectID: id,
|
||||||
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
||||||
UniverseDomainProvider: universeDomainProvider,
|
|
||||||
universeDomain: params.UniverseDomain,
|
universeDomain: params.UniverseDomain,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ func CredentialsFromJSONWithParams(ctx context.Context, jsonData []byte, params
|
|||||||
}
|
}
|
||||||
// Authorized user credentials are only supported in the googleapis.com universe.
|
// Authorized user credentials are only supported in the googleapis.com universe.
|
||||||
if f.Type == userCredentialsKey {
|
if f.Type == userCredentialsKey {
|
||||||
universeDomain = defaultUniverseDomain
|
universeDomain = universeDomainDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
ts, err := f.tokenSource(ctx, params)
|
ts, err := f.tokenSource(ctx, params)
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"cloud.google.com/go/compute/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var saJSONJWT = []byte(`{
|
var saJSONJWT = []byte(`{
|
||||||
@@ -257,14 +255,9 @@ func TestCredentialsFromJSONWithParams_User_UniverseDomain_Params_UniverseDomain
|
|||||||
func TestComputeUniverseDomain(t *testing.T) {
|
func TestComputeUniverseDomain(t *testing.T) {
|
||||||
universeDomainPath := "/computeMetadata/v1/universe/universe_domain"
|
universeDomainPath := "/computeMetadata/v1/universe/universe_domain"
|
||||||
universeDomainResponseBody := "example.com"
|
universeDomainResponseBody := "example.com"
|
||||||
var requests int
|
|
||||||
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
requests++
|
|
||||||
if r.URL.Path != universeDomainPath {
|
if r.URL.Path != universeDomainPath {
|
||||||
t.Errorf("bad path, got %s, want %s", r.URL.Path, universeDomainPath)
|
t.Errorf("got %s, want %s", r.URL.Path, universeDomainPath)
|
||||||
}
|
|
||||||
if requests > 1 {
|
|
||||||
t.Errorf("too many requests, got %d, want 1", requests)
|
|
||||||
}
|
}
|
||||||
w.Write([]byte(universeDomainResponseBody))
|
w.Write([]byte(universeDomainResponseBody))
|
||||||
}))
|
}))
|
||||||
@@ -275,18 +268,10 @@ func TestComputeUniverseDomain(t *testing.T) {
|
|||||||
params := CredentialsParams{
|
params := CredentialsParams{
|
||||||
Scopes: []string{scope},
|
Scopes: []string{scope},
|
||||||
}
|
}
|
||||||
universeDomainProvider := func() (string, error) {
|
|
||||||
universeDomain, err := metadata.Get("universe/universe_domain")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return universeDomain, nil
|
|
||||||
}
|
|
||||||
// Copied from FindDefaultCredentialsWithParams, metadata.OnGCE() = true block
|
// Copied from FindDefaultCredentialsWithParams, metadata.OnGCE() = true block
|
||||||
creds := &Credentials{
|
creds := &Credentials{
|
||||||
ProjectID: "fake_project",
|
ProjectID: "fake_project",
|
||||||
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
TokenSource: computeTokenSource("", params.EarlyTokenRefresh, params.Scopes...),
|
||||||
UniverseDomainProvider: universeDomainProvider,
|
|
||||||
universeDomain: params.UniverseDomain, // empty
|
universeDomain: params.UniverseDomain, // empty
|
||||||
}
|
}
|
||||||
c := make(chan bool)
|
c := make(chan bool)
|
||||||
@@ -300,7 +285,7 @@ func TestComputeUniverseDomain(t *testing.T) {
|
|||||||
}
|
}
|
||||||
c <- true
|
c <- true
|
||||||
}()
|
}()
|
||||||
got, err := creds.GetUniverseDomain() // Second conflicting (and potentially uncached) access.
|
got, err := creds.GetUniverseDomain() // Second conflicting access.
|
||||||
<-c
|
<-c
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
|||||||
@@ -22,9 +22,91 @@
|
|||||||
// the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or
|
// the other by JWTConfigFromJSON. The returned Config can be used to obtain a TokenSource or
|
||||||
// create an http.Client.
|
// create an http.Client.
|
||||||
//
|
//
|
||||||
// # Workload and Workforce Identity Federation
|
// # Workload Identity Federation
|
||||||
//
|
//
|
||||||
// For information on how to use Workload and Workforce Identity Federation, see [golang.org/x/oauth2/google/externalaccount].
|
// Using workload identity federation, your application can access Google Cloud
|
||||||
|
// resources from Amazon Web Services (AWS), Microsoft Azure or any identity
|
||||||
|
// provider that supports OpenID Connect (OIDC) or SAML 2.0.
|
||||||
|
// Traditionally, applications running outside Google Cloud have used service
|
||||||
|
// account keys to access Google Cloud resources. Using identity federation,
|
||||||
|
// you can allow your workload to impersonate a service account.
|
||||||
|
// This lets you access Google Cloud resources directly, eliminating the
|
||||||
|
// maintenance and security burden associated with service account keys.
|
||||||
|
//
|
||||||
|
// Follow the detailed instructions on how to configure Workload Identity Federation
|
||||||
|
// in various platforms:
|
||||||
|
//
|
||||||
|
// 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/workload-identity-federation-with-other-clouds#azure
|
||||||
|
// 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:
|
||||||
|
// 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/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
|
||||||
|
//
|
||||||
|
// # Security considerations
|
||||||
|
//
|
||||||
|
// 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
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
||||||
identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token"
|
identityBindingEndpointTemplate = "https://sts.UNIVERSE_DOMAIN/v1/token"
|
||||||
defaultUniverseDomain = "googleapis.com"
|
universeDomainDefault = "googleapis.com"
|
||||||
)
|
)
|
||||||
|
|
||||||
type accessBoundary struct {
|
type accessBoundary struct {
|
||||||
@@ -117,7 +117,7 @@ type DownscopingConfig struct {
|
|||||||
// configured universe domain.
|
// configured universe domain.
|
||||||
func (dc *DownscopingConfig) identityBindingEndpoint() string {
|
func (dc *DownscopingConfig) identityBindingEndpoint() string {
|
||||||
if dc.UniverseDomain == "" {
|
if dc.UniverseDomain == "" {
|
||||||
return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, defaultUniverseDomain, 1)
|
return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, universeDomainDefault, 1)
|
||||||
}
|
}
|
||||||
return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1)
|
return strings.Replace(identityBindingEndpointTemplate, universeDomainPlaceholder, dc.UniverseDomain, 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,485 +0,0 @@
|
|||||||
// Copyright 2020 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
/*
|
|
||||||
Package externalaccount provides support for creating workload identity
|
|
||||||
federation and workforce identity federation token sources that can be
|
|
||||||
used to access Google Cloud resources from external identity providers.
|
|
||||||
|
|
||||||
# Workload Identity Federation
|
|
||||||
|
|
||||||
Using workload identity federation, your application can access Google Cloud
|
|
||||||
resources from Amazon Web Services (AWS), Microsoft Azure or any identity
|
|
||||||
provider that supports OpenID Connect (OIDC) or SAML 2.0.
|
|
||||||
Traditionally, applications running outside Google Cloud have used service
|
|
||||||
account keys to access Google Cloud resources. Using identity federation,
|
|
||||||
you can allow your workload to impersonate a service account.
|
|
||||||
This lets you access Google Cloud resources directly, eliminating the
|
|
||||||
maintenance and security burden associated with service account keys.
|
|
||||||
|
|
||||||
Follow the detailed instructions on how to configure Workload Identity Federation
|
|
||||||
in various platforms:
|
|
||||||
|
|
||||||
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/workload-identity-federation-with-other-clouds#azure
|
|
||||||
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 fours ways:
|
|
||||||
from a local file location (file-sourced credentials), from a server
|
|
||||||
(URL-sourced credentials), from a local executable (executable-sourced
|
|
||||||
credentials), or from a user defined function that returns an OIDC or SAML token.
|
|
||||||
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/workload-identity-federation-with-other-providers#create_a_credential_configuration
|
|
||||||
|
|
||||||
To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers,
|
|
||||||
or one that implements [AwsSecurityCredentialsSupplier] for AWS providers. This can then be used when building a [Config].
|
|
||||||
The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used to access Google
|
|
||||||
Cloud resources. For instance, you can create a new client from the
|
|
||||||
[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
|
|
||||||
|
|
||||||
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 four ways:
|
|
||||||
from a local file location (file-sourced credentials), from a server
|
|
||||||
(URL-sourced credentials), from a local executable (executable-sourced
|
|
||||||
credentials), or from a user supplied function that returns an OIDC or SAML token.
|
|
||||||
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
|
|
||||||
|
|
||||||
To use a custom function to supply the token, define a struct that implements the [SubjectTokenSupplier] interface for OIDC/SAML providers.
|
|
||||||
This can then be used when building a [Config].
|
|
||||||
The [golang.org/x/oauth2.TokenSource] created from the config using [NewTokenSource] can then be used access Google
|
|
||||||
Cloud resources. For instance, you can create a new client from the
|
|
||||||
[cloud.google.com/go/storage] package and pass in option.WithTokenSource(yourTokenSource))
|
|
||||||
|
|
||||||
# Security considerations
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
package externalaccount
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
"golang.org/x/oauth2/google/internal/impersonate"
|
|
||||||
"golang.org/x/oauth2/google/internal/stsexchange"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
universeDomainPlaceholder = "UNIVERSE_DOMAIN"
|
|
||||||
defaultTokenURL = "https://sts.UNIVERSE_DOMAIN/v1/token"
|
|
||||||
defaultUniverseDomain = "googleapis.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
// now aliases time.Now for testing
|
|
||||||
var now = func() time.Time {
|
|
||||||
return time.Now().UTC()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config stores the configuration for fetching tokens with external credentials.
|
|
||||||
type Config struct {
|
|
||||||
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
|
|
||||||
// identity pool or the workforce pool and the provider identifier in that pool. Required.
|
|
||||||
Audience string
|
|
||||||
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec.
|
|
||||||
// Expected values include:
|
|
||||||
// “urn:ietf:params:oauth:token-type:jwt”
|
|
||||||
// “urn:ietf:params:oauth:token-type:id-token”
|
|
||||||
// “urn:ietf:params:oauth:token-type:saml2”
|
|
||||||
// “urn:ietf:params:aws:token-type:aws4_request”
|
|
||||||
// Required.
|
|
||||||
SubjectTokenType string
|
|
||||||
// TokenURL is the STS token exchange endpoint. If not provided, will default to
|
|
||||||
// https://sts.UNIVERSE_DOMAIN/v1/token, with UNIVERSE_DOMAIN set to the
|
|
||||||
// default service domain googleapis.com unless UniverseDomain is set.
|
|
||||||
// Optional.
|
|
||||||
TokenURL string
|
|
||||||
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
|
|
||||||
// user attributes like account identifier, eg. email, username, uid, etc). This is
|
|
||||||
// needed for gCloud session account identification. Optional.
|
|
||||||
TokenInfoURL string
|
|
||||||
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
|
|
||||||
// required for workload identity pools when APIs to be accessed have not integrated with UberMint. Optional.
|
|
||||||
ServiceAccountImpersonationURL string
|
|
||||||
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
|
|
||||||
// token will be valid for. If not provided, it will default to 3600. Optional.
|
|
||||||
ServiceAccountImpersonationLifetimeSeconds int
|
|
||||||
// ClientSecret is currently only required if token_info endpoint also
|
|
||||||
// needs to be called with the generated GCP access token. When provided, STS will be
|
|
||||||
// called with additional basic authentication using ClientId as username and ClientSecret as password. Optional.
|
|
||||||
ClientSecret string
|
|
||||||
// ClientID is only required in conjunction with ClientSecret, as described above. Optional.
|
|
||||||
ClientID string
|
|
||||||
// CredentialSource contains the necessary information to retrieve the token itself, as well
|
|
||||||
// as some environmental information. One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or
|
|
||||||
// CredentialSource must be provided. Optional.
|
|
||||||
CredentialSource *CredentialSource
|
|
||||||
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
|
|
||||||
// will set the x-goog-user-project header which overrides the project associated with the credentials. Optional.
|
|
||||||
QuotaProjectID string
|
|
||||||
// Scopes contains the desired scopes for the returned access token. Optional.
|
|
||||||
Scopes []string
|
|
||||||
// WorkforcePoolUserProject is the workforce pool user project number when the credential
|
|
||||||
// corresponds to a workforce pool and not a workload identity pool.
|
|
||||||
// The underlying principal must still have serviceusage.services.use IAM
|
|
||||||
// permission to use the project for billing/quota. Optional.
|
|
||||||
WorkforcePoolUserProject string
|
|
||||||
// SubjectTokenSupplier is an optional token supplier for OIDC/SAML credentials.
|
|
||||||
// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
|
|
||||||
SubjectTokenSupplier SubjectTokenSupplier
|
|
||||||
// AwsSecurityCredentialsSupplier is an AWS Security Credential supplier for AWS credentials.
|
|
||||||
// One of SubjectTokenSupplier, AWSSecurityCredentialSupplier or CredentialSource must be provided. Optional.
|
|
||||||
AwsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
|
|
||||||
// UniverseDomain is the default service domain for a given Cloud universe.
|
|
||||||
// This value will be used in the default STS token URL. The default value
|
|
||||||
// is "googleapis.com". It will not be used if TokenURL is set. Optional.
|
|
||||||
UniverseDomain string
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
|
||||||
)
|
|
||||||
|
|
||||||
func validateWorkforceAudience(input string) bool {
|
|
||||||
return validWorkforceAudiencePattern.MatchString(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTokenSource Returns an external account TokenSource using the provided external account config.
|
|
||||||
func NewTokenSource(ctx context.Context, conf Config) (oauth2.TokenSource, error) {
|
|
||||||
if conf.Audience == "" {
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Audience must be set")
|
|
||||||
}
|
|
||||||
if conf.SubjectTokenType == "" {
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Subject token type must be set")
|
|
||||||
}
|
|
||||||
if conf.WorkforcePoolUserProject != "" {
|
|
||||||
valid := validateWorkforceAudience(conf.Audience)
|
|
||||||
if !valid {
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count := 0
|
|
||||||
if conf.CredentialSource != nil {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
if conf.SubjectTokenSupplier != nil {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
if conf.AwsSecurityCredentialsSupplier != nil {
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: One of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
|
|
||||||
}
|
|
||||||
if count > 1 {
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: Only one of CredentialSource, SubjectTokenSupplier, or AwsSecurityCredentialsSupplier must be set")
|
|
||||||
}
|
|
||||||
return conf.tokenSource(ctx, "https")
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenSource is a private function that's directly called by some of the tests,
|
|
||||||
// because the unit test URLs are mocked, and would otherwise fail the
|
|
||||||
// validity check.
|
|
||||||
func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) {
|
|
||||||
|
|
||||||
ts := tokenSource{
|
|
||||||
ctx: ctx,
|
|
||||||
conf: c,
|
|
||||||
}
|
|
||||||
if c.ServiceAccountImpersonationURL == "" {
|
|
||||||
return oauth2.ReuseTokenSource(nil, ts), nil
|
|
||||||
}
|
|
||||||
scopes := c.Scopes
|
|
||||||
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
|
||||||
imp := impersonate.ImpersonateTokenSource{
|
|
||||||
Ctx: ctx,
|
|
||||||
URL: c.ServiceAccountImpersonationURL,
|
|
||||||
Scopes: scopes,
|
|
||||||
Ts: oauth2.ReuseTokenSource(nil, ts),
|
|
||||||
TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
|
|
||||||
}
|
|
||||||
return oauth2.ReuseTokenSource(nil, imp), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subject token file types.
|
|
||||||
const (
|
|
||||||
fileTypeText = "text"
|
|
||||||
fileTypeJSON = "json"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Format contains information needed to retireve a subject token for URL or File sourced credentials.
|
|
||||||
type Format struct {
|
|
||||||
// Type should be either "text" or "json". This determines whether the file or URL sourced credentials
|
|
||||||
// expect a simple text subject token or if the subject token will be contained in a JSON object.
|
|
||||||
// When not provided "text" type is assumed.
|
|
||||||
Type string `json:"type"`
|
|
||||||
// SubjectTokenFieldName is only required for JSON format. This is the field name that the credentials will check
|
|
||||||
// for the subject token in the file or URL response. This would be "access_token" for azure.
|
|
||||||
SubjectTokenFieldName string `json:"subject_token_field_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
|
||||||
type CredentialSource struct {
|
|
||||||
// File is the location for file sourced credentials.
|
|
||||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
|
||||||
File string `json:"file"`
|
|
||||||
|
|
||||||
// Url is the URL to call for URL sourced credentials.
|
|
||||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
|
||||||
URL string `json:"url"`
|
|
||||||
// Headers are the headers to attach to the request for URL sourced credentials.
|
|
||||||
Headers map[string]string `json:"headers"`
|
|
||||||
|
|
||||||
// Executable is the configuration object for executable sourced credentials.
|
|
||||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
|
||||||
Executable *ExecutableConfig `json:"executable"`
|
|
||||||
|
|
||||||
// EnvironmentID is the EnvironmentID used for AWS sourced credentials. This should start with "AWS".
|
|
||||||
// One field amongst File, URL, Executable, or EnvironmentID should be provided, depending on the kind of credential in question.
|
|
||||||
EnvironmentID string `json:"environment_id"`
|
|
||||||
// RegionURL is the metadata URL to retrieve the region from for EC2 AWS credentials.
|
|
||||||
RegionURL string `json:"region_url"`
|
|
||||||
// RegionalCredVerificationURL is the AWS regional credential verification URL, will default to
|
|
||||||
// "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" if not provided."
|
|
||||||
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
|
||||||
// IMDSv2SessionTokenURL is the URL to retrieve the session token when using IMDSv2 in AWS.
|
|
||||||
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
|
|
||||||
// Format is the format type for the subject token. Used for File and URL sourced credentials. Expected values are "text" or "json".
|
|
||||||
Format Format `json:"format"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecutableConfig contains information needed for executable sourced credentials.
|
|
||||||
type ExecutableConfig struct {
|
|
||||||
// Command is the the full command to run to retrieve the subject token.
|
|
||||||
// This can include arguments. Must be an absolute path for the program. Required.
|
|
||||||
Command string `json:"command"`
|
|
||||||
// TimeoutMillis is the timeout duration, in milliseconds. Defaults to 30000 milliseconds when not provided. Optional.
|
|
||||||
TimeoutMillis *int `json:"timeout_millis"`
|
|
||||||
// OutputFile is the absolute path to the output file where the executable will cache the response.
|
|
||||||
// If specified the auth libraries will first check this location before running the executable. Optional.
|
|
||||||
OutputFile string `json:"output_file"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubjectTokenSupplier can be used to supply a subject token to exchange for a GCP access token.
|
|
||||||
type SubjectTokenSupplier interface {
|
|
||||||
// SubjectToken should return a valid subject token or an error.
|
|
||||||
// The external account token source does not cache the returned subject token, so caching
|
|
||||||
// logic should be implemented in the supplier to prevent multiple requests for the same subject token.
|
|
||||||
SubjectToken(ctx context.Context, options SupplierOptions) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AWSSecurityCredentialsSupplier can be used to supply AwsSecurityCredentials and an AWS Region to
|
|
||||||
// exchange for a GCP access token.
|
|
||||||
type AwsSecurityCredentialsSupplier interface {
|
|
||||||
// AwsRegion should return the AWS region or an error.
|
|
||||||
AwsRegion(ctx context.Context, options SupplierOptions) (string, error)
|
|
||||||
// GetAwsSecurityCredentials should return a valid set of AwsSecurityCredentials or an error.
|
|
||||||
// The external account token source does not cache the returned security credentials, so caching
|
|
||||||
// logic should be implemented in the supplier to prevent multiple requests for the same security credentials.
|
|
||||||
AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SupplierOptions contains information about the requested subject token or AWS security credentials from the
|
|
||||||
// Google external account credential.
|
|
||||||
type SupplierOptions struct {
|
|
||||||
// Audience is the requested audience for the external account credential.
|
|
||||||
Audience string
|
|
||||||
// Subject token type is the requested subject token type for the external account credential. Expected values include:
|
|
||||||
// “urn:ietf:params:oauth:token-type:jwt”
|
|
||||||
// “urn:ietf:params:oauth:token-type:id-token”
|
|
||||||
// “urn:ietf:params:oauth:token-type:saml2”
|
|
||||||
// “urn:ietf:params:aws:token-type:aws4_request”
|
|
||||||
SubjectTokenType string
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenURL returns the default STS token endpoint with the configured universe
|
|
||||||
// domain.
|
|
||||||
func (c *Config) tokenURL() string {
|
|
||||||
if c.UniverseDomain == "" {
|
|
||||||
return strings.Replace(defaultTokenURL, universeDomainPlaceholder, defaultUniverseDomain, 1)
|
|
||||||
}
|
|
||||||
return strings.Replace(defaultTokenURL, universeDomainPlaceholder, c.UniverseDomain, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parse determines the type of CredentialSource needed.
|
|
||||||
func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
|
||||||
//set Defaults
|
|
||||||
if c.TokenURL == "" {
|
|
||||||
c.TokenURL = c.tokenURL()
|
|
||||||
}
|
|
||||||
supplierOptions := SupplierOptions{Audience: c.Audience, SubjectTokenType: c.SubjectTokenType}
|
|
||||||
|
|
||||||
if c.AwsSecurityCredentialsSupplier != nil {
|
|
||||||
awsCredSource := awsCredentialSource{
|
|
||||||
awsSecurityCredentialsSupplier: c.AwsSecurityCredentialsSupplier,
|
|
||||||
targetResource: c.Audience,
|
|
||||||
supplierOptions: supplierOptions,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
return awsCredSource, nil
|
|
||||||
} else if c.SubjectTokenSupplier != nil {
|
|
||||||
return programmaticRefreshCredentialSource{subjectTokenSupplier: c.SubjectTokenSupplier, supplierOptions: supplierOptions, ctx: ctx}, nil
|
|
||||||
} else if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
|
|
||||||
if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
|
|
||||||
if awsVersion != 1 {
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: aws version '%d' is not supported in the current build", awsVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
awsCredSource := awsCredentialSource{
|
|
||||||
environmentID: c.CredentialSource.EnvironmentID,
|
|
||||||
regionURL: c.CredentialSource.RegionURL,
|
|
||||||
regionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
|
|
||||||
credVerificationURL: c.CredentialSource.URL,
|
|
||||||
targetResource: c.Audience,
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
if c.CredentialSource.IMDSv2SessionTokenURL != "" {
|
|
||||||
awsCredSource.imdsv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
|
||||||
}
|
|
||||||
|
|
||||||
return awsCredSource, nil
|
|
||||||
}
|
|
||||||
} else if c.CredentialSource.File != "" {
|
|
||||||
return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
|
|
||||||
} else if c.CredentialSource.URL != "" {
|
|
||||||
return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
|
|
||||||
} else if c.CredentialSource.Executable != nil {
|
|
||||||
return createExecutableCredential(ctx, c.CredentialSource.Executable, c)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: unable to parse credential source")
|
|
||||||
}
|
|
||||||
|
|
||||||
type baseCredentialSource interface {
|
|
||||||
credentialSourceType() string
|
|
||||||
subjectToken() (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
|
|
||||||
type tokenSource struct {
|
|
||||||
ctx context.Context
|
|
||||||
conf *Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
|
|
||||||
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
|
|
||||||
goVersion(),
|
|
||||||
"unknown",
|
|
||||||
credSource.credentialSourceType(),
|
|
||||||
conf.ServiceAccountImpersonationURL != "",
|
|
||||||
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Token allows tokenSource to conform to the oauth2.TokenSource interface.
|
|
||||||
func (ts tokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
conf := ts.conf
|
|
||||||
|
|
||||||
credSource, err := conf.parse(ts.ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
subjectToken, err := credSource.subjectToken()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
stsRequest := stsexchange.TokenExchangeRequest{
|
|
||||||
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
|
||||||
Audience: conf.Audience,
|
|
||||||
Scope: conf.Scopes,
|
|
||||||
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
||||||
SubjectToken: subjectToken,
|
|
||||||
SubjectTokenType: conf.SubjectTokenType,
|
|
||||||
}
|
|
||||||
header := make(http.Header)
|
|
||||||
header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
|
|
||||||
clientAuth := stsexchange.ClientAuthentication{
|
|
||||||
AuthStyle: oauth2.AuthStyleInHeader,
|
|
||||||
ClientID: conf.ClientID,
|
|
||||||
ClientSecret: conf.ClientSecret,
|
|
||||||
}
|
|
||||||
var options map[string]interface{}
|
|
||||||
// Do not pass workforce_pool_user_project when client authentication is used.
|
|
||||||
// The client ID is sufficient for determining the user project.
|
|
||||||
if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
|
|
||||||
options = map[string]interface{}{
|
|
||||||
"userProject": conf.WorkforcePoolUserProject,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken := &oauth2.Token{
|
|
||||||
AccessToken: stsResp.AccessToken,
|
|
||||||
TokenType: stsResp.TokenType,
|
|
||||||
}
|
|
||||||
|
|
||||||
// The RFC8693 doesn't define the explicit 0 of "expires_in" field behavior.
|
|
||||||
if stsResp.ExpiresIn <= 0 {
|
|
||||||
return nil, fmt.Errorf("oauth2/google/externalaccount: got invalid expiry from security token service")
|
|
||||||
}
|
|
||||||
accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
|
|
||||||
|
|
||||||
if stsResp.RefreshToken != "" {
|
|
||||||
accessToken.RefreshToken = stsResp.RefreshToken
|
|
||||||
}
|
|
||||||
return accessToken, nil
|
|
||||||
}
|
|
||||||
@@ -1,574 +0,0 @@
|
|||||||
// Copyright 2020 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package externalaccount
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
textBaseCredPath = "testdata/3pi_cred.txt"
|
|
||||||
jsonBaseCredPath = "testdata/3pi_cred.json"
|
|
||||||
baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
|
|
||||||
baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}`
|
|
||||||
)
|
|
||||||
|
|
||||||
var testBaseCredSource = CredentialSource{
|
|
||||||
File: textBaseCredPath,
|
|
||||||
Format: Format{Type: fileTypeText},
|
|
||||||
}
|
|
||||||
|
|
||||||
var testConfig = Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
|
||||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
baseCredsRequestBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
|
||||||
baseCredsResponseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}`
|
|
||||||
workforcePoolRequestBodyWithClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
|
||||||
workforcePoolRequestBodyWithoutClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22userProject%22%3A%22myProject%22%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
|
||||||
correctAT = "Sample.Access.Token"
|
|
||||||
expiry int64 = 234852
|
|
||||||
)
|
|
||||||
var (
|
|
||||||
testNow = func() time.Time { return time.Unix(expiry, 0) }
|
|
||||||
)
|
|
||||||
|
|
||||||
type testExchangeTokenServer struct {
|
|
||||||
url string
|
|
||||||
authorization string
|
|
||||||
contentType string
|
|
||||||
metricsHeader string
|
|
||||||
body string
|
|
||||||
response string
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.Token, error) {
|
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if got, want := r.URL.String(), tets.url; got != want {
|
|
||||||
t.Errorf("URL.String(): got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerAuth := r.Header.Get("Authorization")
|
|
||||||
if got, want := headerAuth, tets.authorization; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerContentType := r.Header.Get("Content-Type")
|
|
||||||
if got, want := headerContentType, tets.contentType; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerMetrics := r.Header.Get("x-goog-api-client")
|
|
||||||
if got, want := headerMetrics, tets.metricsHeader; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed reading request body: %s.", err)
|
|
||||||
}
|
|
||||||
if got, want := string(body), tets.body; got != want {
|
|
||||||
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(tets.response))
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
config.TokenURL = server.URL
|
|
||||||
|
|
||||||
oldNow := now
|
|
||||||
defer func() { now = oldNow }()
|
|
||||||
now = testNow
|
|
||||||
|
|
||||||
ts := tokenSource{
|
|
||||||
ctx: context.Background(),
|
|
||||||
conf: config,
|
|
||||||
}
|
|
||||||
|
|
||||||
return ts.Token()
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateToken(t *testing.T, tok *oauth2.Token, expectToken *oauth2.Token) {
|
|
||||||
if expectToken == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if got, want := tok.AccessToken, expectToken.AccessToken; got != want {
|
|
||||||
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
|
||||||
}
|
|
||||||
if got, want := tok.TokenType, expectToken.TokenType; got != want {
|
|
||||||
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got, want := tok.Expiry, expectToken.Expiry; got != want {
|
|
||||||
t.Errorf("Unexpected Expiry: got %v, but wanted %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server {
|
|
||||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if got, want := r.URL.String(), urlWanted; got != want {
|
|
||||||
t.Errorf("URL.String(): got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerAuth := r.Header.Get("Authorization")
|
|
||||||
if got, want := headerAuth, authWanted; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerContentType := r.Header.Get("Content-Type")
|
|
||||||
if got, want := headerContentType, "application/json"; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed reading request body: %v.", err)
|
|
||||||
}
|
|
||||||
if got, want := string(body), bodyWanted; got != want {
|
|
||||||
t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(response))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server {
|
|
||||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if got, want := r.URL.String(), "/"; got != want {
|
|
||||||
t.Errorf("URL.String(): got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerAuth := r.Header.Get("Authorization")
|
|
||||||
if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerContentType := r.Header.Get("Content-Type")
|
|
||||||
if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
headerMetrics := r.Header.Get("x-goog-api-client")
|
|
||||||
if got, want := headerMetrics, metricsHeaderWanted; got != want {
|
|
||||||
t.Errorf("got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed reading request body: %v.", err)
|
|
||||||
}
|
|
||||||
if got, want := string(body), baseImpersonateCredsReqBody; got != want {
|
|
||||||
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(baseCredsResponseBody))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string {
|
|
||||||
return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToken(t *testing.T) {
|
|
||||||
type MockSTSResponse struct {
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
IssuedTokenType string `json:"issued_token_type"`
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
ExpiresIn int32 `json:"expires_in,omitempty"`
|
|
||||||
Scope string `json:"scopre,omitenpty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
responseBody MockSTSResponse
|
|
||||||
expectToken *oauth2.Token
|
|
||||||
expectErrorMsg string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "happy case",
|
|
||||||
responseBody: MockSTSResponse{
|
|
||||||
AccessToken: correctAT,
|
|
||||||
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
||||||
TokenType: "Bearer",
|
|
||||||
ExpiresIn: 3600,
|
|
||||||
Scope: "https://www.googleapis.com/auth/cloud-platform",
|
|
||||||
},
|
|
||||||
expectToken: &oauth2.Token{
|
|
||||||
AccessToken: correctAT,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: testNow().Add(time.Duration(3600) * time.Second),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no expiry time on token",
|
|
||||||
responseBody: MockSTSResponse{
|
|
||||||
AccessToken: correctAT,
|
|
||||||
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Scope: "https://www.googleapis.com/auth/cloud-platform",
|
|
||||||
},
|
|
||||||
expectToken: nil,
|
|
||||||
expectErrorMsg: "oauth2/google/externalaccount: got invalid expiry from security token service",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "negative expiry time",
|
|
||||||
responseBody: MockSTSResponse{
|
|
||||||
AccessToken: correctAT,
|
|
||||||
IssuedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
|
||||||
TokenType: "Bearer",
|
|
||||||
ExpiresIn: -1,
|
|
||||||
Scope: "https://www.googleapis.com/auth/cloud-platform",
|
|
||||||
},
|
|
||||||
expectToken: nil,
|
|
||||||
expectErrorMsg: "oauth2/google/externalaccount: got invalid expiry from security token service",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
|
||||||
config := Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
}
|
|
||||||
|
|
||||||
responseBody, err := json.Marshal(testCase.responseBody)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Invalid response received.")
|
|
||||||
}
|
|
||||||
|
|
||||||
server := testExchangeTokenServer{
|
|
||||||
url: "/",
|
|
||||||
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
|
||||||
contentType: "application/x-www-form-urlencoded",
|
|
||||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
|
||||||
body: baseCredsRequestBody,
|
|
||||||
response: string(responseBody),
|
|
||||||
}
|
|
||||||
|
|
||||||
tok, err := run(t, &config, &server)
|
|
||||||
|
|
||||||
if err != nil && err.Error() != testCase.expectErrorMsg {
|
|
||||||
t.Errorf("Error not as expected: got = %v, and want = %v", err, testCase.expectErrorMsg)
|
|
||||||
}
|
|
||||||
validateToken(t, tok, testCase.expectToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkforcePoolTokenWithClientID(t *testing.T) {
|
|
||||||
config := Config{
|
|
||||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
WorkforcePoolUserProject: "myProject",
|
|
||||||
}
|
|
||||||
|
|
||||||
server := testExchangeTokenServer{
|
|
||||||
url: "/",
|
|
||||||
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
|
||||||
contentType: "application/x-www-form-urlencoded",
|
|
||||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
|
||||||
body: workforcePoolRequestBodyWithClientId,
|
|
||||||
response: baseCredsResponseBody,
|
|
||||||
}
|
|
||||||
|
|
||||||
tok, err := run(t, &config, &server)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %e", err)
|
|
||||||
}
|
|
||||||
expectToken := oauth2.Token{
|
|
||||||
AccessToken: correctAT,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: testNow().Add(time.Duration(3600) * time.Second),
|
|
||||||
}
|
|
||||||
validateToken(t, tok, &expectToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
|
|
||||||
config := Config{
|
|
||||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
WorkforcePoolUserProject: "myProject",
|
|
||||||
}
|
|
||||||
|
|
||||||
server := testExchangeTokenServer{
|
|
||||||
url: "/",
|
|
||||||
authorization: "",
|
|
||||||
contentType: "application/x-www-form-urlencoded",
|
|
||||||
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
|
||||||
body: workforcePoolRequestBodyWithoutClientId,
|
|
||||||
response: baseCredsResponseBody,
|
|
||||||
}
|
|
||||||
|
|
||||||
tok, err := run(t, &config, &server)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %e", err)
|
|
||||||
}
|
|
||||||
expectToken := oauth2.Token{
|
|
||||||
AccessToken: correctAT,
|
|
||||||
TokenType: "Bearer",
|
|
||||||
Expiry: testNow().Add(time.Duration(3600) * time.Second),
|
|
||||||
}
|
|
||||||
validateToken(t, tok, &expectToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) {
|
|
||||||
config := Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
|
||||||
TokenURL: "https://sts.googleapis.com",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
WorkforcePoolUserProject: "myProject",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := NewTokenSource(context.Background(), config)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("Expected error but found none")
|
|
||||||
}
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: Workforce pool user project should not be set for non-workforce pool credentials"; got != want {
|
|
||||||
t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWorkforcePoolCreation(t *testing.T) {
|
|
||||||
var audienceValidatyTests = []struct {
|
|
||||||
audience string
|
|
||||||
expectSuccess bool
|
|
||||||
}{
|
|
||||||
{"//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id", true},
|
|
||||||
{"//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", true},
|
|
||||||
{"//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", true},
|
|
||||||
{"identitynamespace:1f12345:my_provider", false},
|
|
||||||
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/pool-id/providers/provider-id", false},
|
|
||||||
{"//iam.googleapis.com/projects/123456/locations/eu/workloadIdentityPools/pool-id/providers/provider-id", false},
|
|
||||||
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/workforcePools/providers/provider-id", false},
|
|
||||||
{"//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
|
||||||
{"//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
|
||||||
{"//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", false},
|
|
||||||
{"//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", false},
|
|
||||||
{"//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
for _, tt := range audienceValidatyTests {
|
|
||||||
t.Run(" "+tt.audience, 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 = "https://iamcredentials.googleapis.com"
|
|
||||||
config.Audience = tt.audience
|
|
||||||
config.WorkforcePoolUserProject = "myProject"
|
|
||||||
_, err := NewTokenSource(ctx, config)
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var impersonationTests = []struct {
|
|
||||||
name string
|
|
||||||
config Config
|
|
||||||
expectedImpersonationBody string
|
|
||||||
expectedMetricsHeader string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Base Impersonation",
|
|
||||||
config: Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
|
||||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
},
|
|
||||||
expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
|
|
||||||
expectedMetricsHeader: getExpectedMetricsHeader("file", true, false),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "With TokenLifetime Set",
|
|
||||||
config: Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
|
||||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
|
||||||
},
|
|
||||||
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
|
|
||||||
expectedMetricsHeader: getExpectedMetricsHeader("file", true, true),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestImpersonation(t *testing.T) {
|
|
||||||
for _, tt := range impersonationTests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
testImpersonateConfig := tt.config
|
|
||||||
impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t)
|
|
||||||
defer impersonateServer.Close()
|
|
||||||
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
|
|
||||||
|
|
||||||
targetServer := createTargetServer(tt.expectedMetricsHeader, t)
|
|
||||||
defer targetServer.Close()
|
|
||||||
testImpersonateConfig.TokenURL = targetServer.URL
|
|
||||||
|
|
||||||
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to create TokenSource: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
oldNow := now
|
|
||||||
defer func() { now = oldNow }()
|
|
||||||
now = testNow
|
|
||||||
|
|
||||||
tok, err := ourTS.Token()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Unexpected error: %e", err)
|
|
||||||
}
|
|
||||||
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
|
|
||||||
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
|
||||||
}
|
|
||||||
if got, want := tok.TokenType, "Bearer"; got != want {
|
|
||||||
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newTokenTests = []struct {
|
|
||||||
name string
|
|
||||||
config Config
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Missing Audience",
|
|
||||||
config: Config{
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
|
||||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Missing Subject Token Type",
|
|
||||||
config: Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "No Cred Source",
|
|
||||||
config: Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
|
||||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Cred Source and Supplier",
|
|
||||||
config: Config{
|
|
||||||
Audience: "32555940559.apps.googleusercontent.com",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
|
||||||
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
AwsSecurityCredentialsSupplier: testAwsSupplier{},
|
|
||||||
ClientSecret: "notsosecret",
|
|
||||||
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewToken(t *testing.T) {
|
|
||||||
for _, tt := range newTokenTests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
testConfig := tt.config
|
|
||||||
|
|
||||||
_, err := NewTokenSource(context.Background(), testConfig)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error when calling NewToken()")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfig_TokenURL(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
tokenURL string
|
|
||||||
universeDomain string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
tokenURL: "https://sts.googleapis.com/v1/token",
|
|
||||||
universeDomain: "",
|
|
||||||
want: "https://sts.googleapis.com/v1/token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokenURL: "",
|
|
||||||
universeDomain: "",
|
|
||||||
want: "https://sts.googleapis.com/v1/token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokenURL: "",
|
|
||||||
universeDomain: "googleapis.com",
|
|
||||||
want: "https://sts.googleapis.com/v1/token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
tokenURL: "",
|
|
||||||
universeDomain: "example.com",
|
|
||||||
want: "https://sts.example.com/v1/token",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
config := &Config{
|
|
||||||
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
|
||||||
CredentialSource: &testBaseCredSource,
|
|
||||||
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
|
||||||
}
|
|
||||||
config.TokenURL = tt.tokenURL
|
|
||||||
config.UniverseDomain = tt.universeDomain
|
|
||||||
config.parse(context.Background())
|
|
||||||
if got := config.TokenURL; got != tt.want {
|
|
||||||
t.Errorf("got %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
// Copyright 2024 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package externalaccount
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type programmaticRefreshCredentialSource struct {
|
|
||||||
supplierOptions SupplierOptions
|
|
||||||
subjectTokenSupplier SubjectTokenSupplier
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs programmaticRefreshCredentialSource) credentialSourceType() string {
|
|
||||||
return "programmatic"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs programmaticRefreshCredentialSource) subjectToken() (string, error) {
|
|
||||||
return cs.subjectTokenSupplier.SubjectToken(cs.ctx, cs.supplierOptions)
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
// Copyright 2024 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package externalaccount
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRetrieveSubjectToken_ProgrammaticAuth(t *testing.T) {
|
|
||||||
tfc := testConfig
|
|
||||||
|
|
||||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
|
||||||
subjectToken: "subjectToken",
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if out != "subjectToken" {
|
|
||||||
t.Errorf("subjectToken = \n%q\n want \nSubjectToken", out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetrieveSubjectToken_ProgrammaticAuthFails(t *testing.T) {
|
|
||||||
tfc := testConfig
|
|
||||||
testError := errors.New("test error")
|
|
||||||
|
|
||||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
|
||||||
err: testError,
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse() failed %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = base.subjectToken()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("subjectToken() should have failed")
|
|
||||||
}
|
|
||||||
if testError != err {
|
|
||||||
t.Errorf("subjectToken = %e, want %e", err, testError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetrieveSubjectToken_ProgrammaticAuthOptions(t *testing.T) {
|
|
||||||
tfc := testConfig
|
|
||||||
expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType}
|
|
||||||
|
|
||||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
|
||||||
subjectToken: "subjectToken",
|
|
||||||
expectedOptions: &expectedOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse() failed %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = base.subjectToken()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRetrieveSubjectToken_ProgrammaticAuthContext(t *testing.T) {
|
|
||||||
tfc := testConfig
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
tfc.SubjectTokenSupplier = testSubjectTokenSupplier{
|
|
||||||
subjectToken: "subjectToken",
|
|
||||||
expectedContext: ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := tfc.parse(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse() failed %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = base.subjectToken()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("retrieveSubjectToken() failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type testSubjectTokenSupplier struct {
|
|
||||||
err error
|
|
||||||
subjectToken string
|
|
||||||
expectedOptions *SupplierOptions
|
|
||||||
expectedContext context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (supp testSubjectTokenSupplier) SubjectToken(ctx context.Context, options SupplierOptions) (string, error) {
|
|
||||||
if supp.err != nil {
|
|
||||||
return "", supp.err
|
|
||||||
}
|
|
||||||
if supp.expectedOptions != nil {
|
|
||||||
if supp.expectedOptions.Audience != options.Audience {
|
|
||||||
return "", errors.New("Audience does not match")
|
|
||||||
}
|
|
||||||
if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType {
|
|
||||||
return "", errors.New("Audience does not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if supp.expectedContext != nil {
|
|
||||||
if supp.expectedContext != ctx {
|
|
||||||
return "", errors.New("Context does not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return supp.subjectToken, nil
|
|
||||||
}
|
|
||||||
@@ -15,9 +15,8 @@ import (
|
|||||||
|
|
||||||
"cloud.google.com/go/compute/metadata"
|
"cloud.google.com/go/compute/metadata"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/google/externalaccount"
|
"golang.org/x/oauth2/google/internal/externalaccount"
|
||||||
"golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
|
"golang.org/x/oauth2/google/internal/externalaccountauthorizeduser"
|
||||||
"golang.org/x/oauth2/google/internal/impersonate"
|
|
||||||
"golang.org/x/oauth2/jwt"
|
"golang.org/x/oauth2/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -201,12 +200,12 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
|
|||||||
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
|
ServiceAccountImpersonationLifetimeSeconds: f.ServiceAccountImpersonation.TokenLifetimeSeconds,
|
||||||
ClientSecret: f.ClientSecret,
|
ClientSecret: f.ClientSecret,
|
||||||
ClientID: f.ClientID,
|
ClientID: f.ClientID,
|
||||||
CredentialSource: &f.CredentialSource,
|
CredentialSource: f.CredentialSource,
|
||||||
QuotaProjectID: f.QuotaProjectID,
|
QuotaProjectID: f.QuotaProjectID,
|
||||||
Scopes: params.Scopes,
|
Scopes: params.Scopes,
|
||||||
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
|
WorkforcePoolUserProject: f.WorkforcePoolUserProject,
|
||||||
}
|
}
|
||||||
return externalaccount.NewTokenSource(ctx, *cfg)
|
return cfg.TokenSource(ctx)
|
||||||
case externalAccountAuthorizedUserKey:
|
case externalAccountAuthorizedUserKey:
|
||||||
cfg := &externalaccountauthorizeduser.Config{
|
cfg := &externalaccountauthorizeduser.Config{
|
||||||
Audience: f.Audience,
|
Audience: f.Audience,
|
||||||
@@ -229,7 +228,7 @@ func (f *credentialsFile) tokenSource(ctx context.Context, params CredentialsPar
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
imp := impersonate.ImpersonateTokenSource{
|
imp := externalaccount.ImpersonateTokenSource{
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
URL: f.ServiceAccountImpersonationURL,
|
URL: f.ServiceAccountImpersonationURL,
|
||||||
Scopes: params.Scopes,
|
Scopes: params.Scopes,
|
||||||
|
|||||||
@@ -26,28 +26,22 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AwsSecurityCredentials models AWS security credentials.
|
type awsSecurityCredentials struct {
|
||||||
type AwsSecurityCredentials struct {
|
|
||||||
// AccessKeyId is the AWS Access Key ID - Required.
|
|
||||||
AccessKeyID string `json:"AccessKeyID"`
|
AccessKeyID string `json:"AccessKeyID"`
|
||||||
// SecretAccessKey is the AWS Secret Access Key - Required.
|
|
||||||
SecretAccessKey string `json:"SecretAccessKey"`
|
SecretAccessKey string `json:"SecretAccessKey"`
|
||||||
// SessionToken is the AWS Session token. This should be provided for temporary AWS security credentials - Optional.
|
SecurityToken string `json:"Token"`
|
||||||
SessionToken string `json:"Token"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
|
// awsRequestSigner is a utility class to sign http requests using a AWS V4 signature.
|
||||||
type awsRequestSigner struct {
|
type awsRequestSigner struct {
|
||||||
RegionName string
|
RegionName string
|
||||||
AwsSecurityCredentials *AwsSecurityCredentials
|
AwsSecurityCredentials awsSecurityCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
// getenv aliases os.Getenv for testing
|
// getenv aliases os.Getenv for testing
|
||||||
var getenv = os.Getenv
|
var getenv = os.Getenv
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultRegionalCredentialVerificationUrl = "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
|
|
||||||
|
|
||||||
// AWS Signature Version 4 signing algorithm identifier.
|
// AWS Signature Version 4 signing algorithm identifier.
|
||||||
awsAlgorithm = "AWS4-HMAC-SHA256"
|
awsAlgorithm = "AWS4-HMAC-SHA256"
|
||||||
|
|
||||||
@@ -203,8 +197,8 @@ func (rs *awsRequestSigner) SignRequest(req *http.Request) error {
|
|||||||
|
|
||||||
signedRequest.Header.Add("host", requestHost(req))
|
signedRequest.Header.Add("host", requestHost(req))
|
||||||
|
|
||||||
if rs.AwsSecurityCredentials.SessionToken != "" {
|
if rs.AwsSecurityCredentials.SecurityToken != "" {
|
||||||
signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SessionToken)
|
signedRequest.Header.Add(awsSecurityTokenHeader, rs.AwsSecurityCredentials.SecurityToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
if signedRequest.Header.Get("date") == "" {
|
if signedRequest.Header.Get("date") == "" {
|
||||||
@@ -257,18 +251,16 @@ func (rs *awsRequestSigner) generateAuthentication(req *http.Request, timestamp
|
|||||||
}
|
}
|
||||||
|
|
||||||
type awsCredentialSource struct {
|
type awsCredentialSource struct {
|
||||||
environmentID string
|
EnvironmentID string
|
||||||
regionURL string
|
RegionURL string
|
||||||
regionalCredVerificationURL string
|
RegionalCredVerificationURL string
|
||||||
credVerificationURL string
|
CredVerificationURL string
|
||||||
imdsv2SessionTokenURL string
|
IMDSv2SessionTokenURL string
|
||||||
targetResource string
|
TargetResource string
|
||||||
requestSigner *awsRequestSigner
|
requestSigner *awsRequestSigner
|
||||||
region string
|
region string
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
client *http.Client
|
client *http.Client
|
||||||
awsSecurityCredentialsSupplier AwsSecurityCredentialsSupplier
|
|
||||||
supplierOptions SupplierOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type awsRequestHeader struct {
|
type awsRequestHeader struct {
|
||||||
@@ -300,25 +292,18 @@ func canRetrieveSecurityCredentialFromEnvironment() bool {
|
|||||||
return getenv(awsAccessKeyId) != "" && getenv(awsSecretAccessKey) != ""
|
return getenv(awsAccessKeyId) != "" && getenv(awsSecretAccessKey) != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs awsCredentialSource) shouldUseMetadataServer() bool {
|
func shouldUseMetadataServer() bool {
|
||||||
return cs.awsSecurityCredentialsSupplier == nil && (!canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment())
|
return !canRetrieveRegionFromEnvironment() || !canRetrieveSecurityCredentialFromEnvironment()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs awsCredentialSource) credentialSourceType() string {
|
func (cs awsCredentialSource) credentialSourceType() string {
|
||||||
if cs.awsSecurityCredentialsSupplier != nil {
|
|
||||||
return "programmatic"
|
|
||||||
}
|
|
||||||
return "aws"
|
return "aws"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs awsCredentialSource) subjectToken() (string, error) {
|
func (cs awsCredentialSource) subjectToken() (string, error) {
|
||||||
// Set Defaults
|
|
||||||
if cs.regionalCredVerificationURL == "" {
|
|
||||||
cs.regionalCredVerificationURL = defaultRegionalCredentialVerificationUrl
|
|
||||||
}
|
|
||||||
if cs.requestSigner == nil {
|
if cs.requestSigner == nil {
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
if cs.shouldUseMetadataServer() {
|
if shouldUseMetadataServer() {
|
||||||
awsSessionToken, err := cs.getAWSSessionToken()
|
awsSessionToken, err := cs.getAWSSessionToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -333,8 +318,8 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
cs.region, err = cs.getRegion(headers)
|
|
||||||
if err != nil {
|
if cs.region, err = cs.getRegion(headers); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +331,7 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
|||||||
|
|
||||||
// Generate the signed request to AWS STS GetCallerIdentity API.
|
// Generate the signed request to AWS STS GetCallerIdentity API.
|
||||||
// Use the required regional endpoint. Otherwise, the request will fail.
|
// Use the required regional endpoint. Otherwise, the request will fail.
|
||||||
req, err := http.NewRequest("POST", strings.Replace(cs.regionalCredVerificationURL, "{region}", cs.region, 1), nil)
|
req, err := http.NewRequest("POST", strings.Replace(cs.RegionalCredVerificationURL, "{region}", cs.region, 1), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -354,8 +339,8 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
|||||||
// provider, with or without the HTTPS prefix.
|
// provider, with or without the HTTPS prefix.
|
||||||
// Including this header as part of the signature is recommended to
|
// Including this header as part of the signature is recommended to
|
||||||
// ensure data integrity.
|
// ensure data integrity.
|
||||||
if cs.targetResource != "" {
|
if cs.TargetResource != "" {
|
||||||
req.Header.Add("x-goog-cloud-target-resource", cs.targetResource)
|
req.Header.Add("x-goog-cloud-target-resource", cs.TargetResource)
|
||||||
}
|
}
|
||||||
cs.requestSigner.SignRequest(req)
|
cs.requestSigner.SignRequest(req)
|
||||||
|
|
||||||
@@ -402,11 +387,11 @@ func (cs awsCredentialSource) subjectToken() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
|
func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
|
||||||
if cs.imdsv2SessionTokenURL == "" {
|
if cs.IMDSv2SessionTokenURL == "" {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", cs.imdsv2SessionTokenURL, nil)
|
req, err := http.NewRequest("PUT", cs.IMDSv2SessionTokenURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -425,29 +410,25 @@ func (cs *awsCredentialSource) getAWSSessionToken() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS session token - %s", string(respBody))
|
return "", fmt.Errorf("oauth2/google: unable to retrieve AWS session token - %s", string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(respBody), nil
|
return string(respBody), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) {
|
func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, error) {
|
||||||
if cs.awsSecurityCredentialsSupplier != nil {
|
|
||||||
return cs.awsSecurityCredentialsSupplier.AwsRegion(cs.ctx, cs.supplierOptions)
|
|
||||||
}
|
|
||||||
if canRetrieveRegionFromEnvironment() {
|
if canRetrieveRegionFromEnvironment() {
|
||||||
if envAwsRegion := getenv(awsRegion); envAwsRegion != "" {
|
if envAwsRegion := getenv(awsRegion); envAwsRegion != "" {
|
||||||
cs.region = envAwsRegion
|
|
||||||
return envAwsRegion, nil
|
return envAwsRegion, nil
|
||||||
}
|
}
|
||||||
return getenv("AWS_DEFAULT_REGION"), nil
|
return getenv("AWS_DEFAULT_REGION"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cs.regionURL == "" {
|
if cs.RegionURL == "" {
|
||||||
return "", errors.New("oauth2/google/externalaccount: unable to determine AWS region")
|
return "", errors.New("oauth2/google: unable to determine AWS region")
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", cs.regionURL, nil)
|
req, err := http.NewRequest("GET", cs.RegionURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -468,7 +449,7 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS region - %s", string(respBody))
|
return "", fmt.Errorf("oauth2/google: unable to retrieve AWS region - %s", string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
// This endpoint will return the region in format: us-east-2b.
|
// This endpoint will return the region in format: us-east-2b.
|
||||||
@@ -480,15 +461,12 @@ func (cs *awsCredentialSource) getRegion(headers map[string]string) (string, err
|
|||||||
return string(respBody[:respBodyEnd]), nil
|
return string(respBody[:respBodyEnd]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
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 cs.awsSecurityCredentialsSupplier != nil {
|
|
||||||
return cs.awsSecurityCredentialsSupplier.AwsSecurityCredentials(cs.ctx, cs.supplierOptions)
|
|
||||||
}
|
|
||||||
if canRetrieveSecurityCredentialFromEnvironment() {
|
if canRetrieveSecurityCredentialFromEnvironment() {
|
||||||
return &AwsSecurityCredentials{
|
return awsSecurityCredentials{
|
||||||
AccessKeyID: getenv(awsAccessKeyId),
|
AccessKeyID: getenv(awsAccessKeyId),
|
||||||
SecretAccessKey: getenv(awsSecretAccessKey),
|
SecretAccessKey: getenv(awsSecretAccessKey),
|
||||||
SessionToken: getenv(awsSessionToken),
|
SecurityToken: getenv(awsSessionToken),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,23 +481,24 @@ func (cs *awsCredentialSource) getSecurityCredentials(headers map[string]string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if credentials.AccessKeyID == "" {
|
if credentials.AccessKeyID == "" {
|
||||||
return result, errors.New("oauth2/google/externalaccount: missing AccessKeyId credential")
|
return result, errors.New("oauth2/google: missing AccessKeyId credential")
|
||||||
}
|
}
|
||||||
|
|
||||||
if credentials.SecretAccessKey == "" {
|
if credentials.SecretAccessKey == "" {
|
||||||
return result, errors.New("oauth2/google/externalaccount: missing SecretAccessKey credential")
|
return result, errors.New("oauth2/google: missing SecretAccessKey credential")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &credentials, nil
|
return credentials, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (AwsSecurityCredentials, error) {
|
func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, headers map[string]string) (awsSecurityCredentials, error) {
|
||||||
var result AwsSecurityCredentials
|
var result awsSecurityCredentials
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.credVerificationURL, roleName), nil)
|
req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s", cs.CredVerificationURL, roleName), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
for name, value := range headers {
|
for name, value := range headers {
|
||||||
req.Header.Add(name, value)
|
req.Header.Add(name, value)
|
||||||
@@ -537,7 +516,7 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return result, fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS security credentials - %s", string(respBody))
|
return result, fmt.Errorf("oauth2/google: unable to retrieve AWS security credentials - %s", string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(respBody, &result)
|
err = json.Unmarshal(respBody, &result)
|
||||||
@@ -545,11 +524,11 @@ func (cs *awsCredentialSource) getMetadataSecurityCredentials(roleName string, h
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (string, error) {
|
func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (string, error) {
|
||||||
if cs.credVerificationURL == "" {
|
if cs.CredVerificationURL == "" {
|
||||||
return "", errors.New("oauth2/google/externalaccount: unable to determine the AWS metadata server security credentials endpoint")
|
return "", errors.New("oauth2/google: unable to determine the AWS metadata server security credentials endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", cs.credVerificationURL, nil)
|
req, err := http.NewRequest("GET", cs.CredVerificationURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -570,7 +549,7 @@ func (cs *awsCredentialSource) getMetadataRoleName(headers map[string]string) (s
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: unable to retrieve AWS role name - %s", string(respBody))
|
return "", fmt.Errorf("oauth2/google: unable to retrieve AWS role name - %s", string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(respBody), nil
|
return string(respBody), nil
|
||||||
@@ -7,7 +7,6 @@ package externalaccount
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -37,7 +36,7 @@ func setEnvironment(env map[string]string) func(string) string {
|
|||||||
|
|
||||||
var defaultRequestSigner = &awsRequestSigner{
|
var defaultRequestSigner = &awsRequestSigner{
|
||||||
RegionName: "us-east-1",
|
RegionName: "us-east-1",
|
||||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
AwsSecurityCredentials: awsSecurityCredentials{
|
||||||
AccessKeyID: "AKIDEXAMPLE",
|
AccessKeyID: "AKIDEXAMPLE",
|
||||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||||
},
|
},
|
||||||
@@ -51,10 +50,10 @@ const (
|
|||||||
|
|
||||||
var requestSignerWithToken = &awsRequestSigner{
|
var requestSignerWithToken = &awsRequestSigner{
|
||||||
RegionName: "us-east-2",
|
RegionName: "us-east-2",
|
||||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
AwsSecurityCredentials: awsSecurityCredentials{
|
||||||
AccessKeyID: accessKeyID,
|
AccessKeyID: accessKeyID,
|
||||||
SecretAccessKey: secretAccessKey,
|
SecretAccessKey: secretAccessKey,
|
||||||
SessionToken: securityToken,
|
SecurityToken: securityToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,7 +388,7 @@ func TestAWSv4Signature_PostRequestWithSecurityTokenAndAdditionalHeaders(t *test
|
|||||||
func TestAWSv4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) {
|
func TestAWSv4Signature_PostRequestWithAmzDateButNoSecurityToken(t *testing.T) {
|
||||||
var requestSigner = &awsRequestSigner{
|
var requestSigner = &awsRequestSigner{
|
||||||
RegionName: "us-east-2",
|
RegionName: "us-east-2",
|
||||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
AwsSecurityCredentials: awsSecurityCredentials{
|
||||||
AccessKeyID: accessKeyID,
|
AccessKeyID: accessKeyID,
|
||||||
SecretAccessKey: secretAccessKey,
|
SecretAccessKey: secretAccessKey,
|
||||||
},
|
},
|
||||||
@@ -527,8 +526,8 @@ func notFound(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func noHeaderValidation(r *http.Request) {}
|
func noHeaderValidation(r *http.Request) {}
|
||||||
|
|
||||||
func (server *testAwsServer) getCredentialSource(url string) *CredentialSource {
|
func (server *testAwsServer) getCredentialSource(url string) CredentialSource {
|
||||||
return &CredentialSource{
|
return CredentialSource{
|
||||||
EnvironmentID: "aws1",
|
EnvironmentID: "aws1",
|
||||||
URL: url + server.url,
|
URL: url + server.url,
|
||||||
RegionURL: url + server.regionURL,
|
RegionURL: url + server.regionURL,
|
||||||
@@ -542,10 +541,10 @@ func getExpectedSubjectToken(url, region, accessKeyID, secretAccessKey, security
|
|||||||
req.Header.Add("x-goog-cloud-target-resource", testFileConfig.Audience)
|
req.Header.Add("x-goog-cloud-target-resource", testFileConfig.Audience)
|
||||||
signer := &awsRequestSigner{
|
signer := &awsRequestSigner{
|
||||||
RegionName: region,
|
RegionName: region,
|
||||||
AwsSecurityCredentials: &AwsSecurityCredentials{
|
AwsSecurityCredentials: awsSecurityCredentials{
|
||||||
AccessKeyID: accessKeyID,
|
AccessKeyID: accessKeyID,
|
||||||
SecretAccessKey: secretAccessKey,
|
SecretAccessKey: secretAccessKey,
|
||||||
SessionToken: securityToken,
|
SecurityToken: securityToken,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
signer.SignRequest(req)
|
signer.SignRequest(req)
|
||||||
@@ -589,6 +588,7 @@ func TestAWSCredential_BasicRequest(t *testing.T) {
|
|||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
tfc.CredentialSource = server.getCredentialSource(ts.URL)
|
||||||
|
|
||||||
oldGetenv := getenv
|
oldGetenv := getenv
|
||||||
oldNow := now
|
oldNow := now
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -846,7 +846,7 @@ func TestAWSCredential_RequestWithBadVersion(t *testing.T) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("parse() should have failed")
|
t.Fatalf("parse() should have failed")
|
||||||
}
|
}
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: aws version '3' is not supported in the current build"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: aws version '3' is not supported in the current build"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -875,7 +875,7 @@ func TestAWSCredential_RequestWithNoRegionURL(t *testing.T) {
|
|||||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to determine AWS region"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: unable to determine AWS region"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -905,7 +905,7 @@ func TestAWSCredential_RequestWithBadRegionURL(t *testing.T) {
|
|||||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS region - Not Found"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: unable to retrieve AWS region - Not Found"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -937,7 +937,7 @@ func TestAWSCredential_RequestWithMissingCredential(t *testing.T) {
|
|||||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: missing AccessKeyId credential"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: missing AccessKeyId credential"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,7 +969,7 @@ func TestAWSCredential_RequestWithIncompleteCredential(t *testing.T) {
|
|||||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: missing SecretAccessKey credential"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: missing SecretAccessKey credential"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -998,7 +998,7 @@ func TestAWSCredential_RequestWithNoCredentialURL(t *testing.T) {
|
|||||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to determine the AWS metadata server security credentials endpoint"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: unable to determine the AWS metadata server security credentials endpoint"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1027,7 +1027,7 @@ func TestAWSCredential_RequestWithBadCredentialURL(t *testing.T) {
|
|||||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS role name - Not Found"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: unable to retrieve AWS role name - Not Found"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1056,7 +1056,7 @@ func TestAWSCredential_RequestWithBadFinalCredentialURL(t *testing.T) {
|
|||||||
t.Fatalf("retrieveSubjectToken() should have failed")
|
t.Fatalf("retrieveSubjectToken() should have failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if got, want := err.Error(), "oauth2/google/externalaccount: unable to retrieve AWS security credentials - Not Found"; !reflect.DeepEqual(got, want) {
|
if got, want := err.Error(), "oauth2/google: unable to retrieve AWS security credentials - Not Found"; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("subjectToken = %q, want %q", got, want)
|
t.Errorf("subjectToken = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1235,192 +1235,6 @@ func TestAWSCredential_ShouldCallMetadataEndpointWhenNoSecretAccessKey(t *testin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAWSCredential_ProgrammaticAuth(t *testing.T) {
|
|
||||||
tfc := testFileConfig
|
|
||||||
securityCredentials := AwsSecurityCredentials{
|
|
||||||
AccessKeyID: accessKeyID,
|
|
||||||
SecretAccessKey: secretAccessKey,
|
|
||||||
SessionToken: securityToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
|
||||||
awsRegion: "us-east-2",
|
|
||||||
err: nil,
|
|
||||||
credentials: &securityCredentials,
|
|
||||||
}
|
|
||||||
|
|
||||||
oldNow := now
|
|
||||||
defer func() {
|
|
||||||
now = oldNow
|
|
||||||
}()
|
|
||||||
now = setTime(defaultTime)
|
|
||||||
|
|
||||||
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,
|
|
||||||
securityToken,
|
|
||||||
)
|
|
||||||
|
|
||||||
if got, want := out, expected; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Errorf("subjectToken = \n%q\n want \n%q", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAWSCredential_ProgrammaticAuthNoSessionToken(t *testing.T) {
|
|
||||||
tfc := testFileConfig
|
|
||||||
securityCredentials := AwsSecurityCredentials{
|
|
||||||
AccessKeyID: accessKeyID,
|
|
||||||
SecretAccessKey: secretAccessKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
|
||||||
awsRegion: "us-east-2",
|
|
||||||
err: nil,
|
|
||||||
credentials: &securityCredentials,
|
|
||||||
}
|
|
||||||
|
|
||||||
oldNow := now
|
|
||||||
defer func() {
|
|
||||||
now = oldNow
|
|
||||||
}()
|
|
||||||
now = setTime(defaultTime)
|
|
||||||
|
|
||||||
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_ProgrammaticAuthError(t *testing.T) {
|
|
||||||
tfc := testFileConfig
|
|
||||||
testErr := errors.New("test error")
|
|
||||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
|
||||||
awsRegion: "us-east-2",
|
|
||||||
err: testErr,
|
|
||||||
credentials: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse() failed %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = base.subjectToken()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("subjectToken() should have failed")
|
|
||||||
}
|
|
||||||
if err != testErr {
|
|
||||||
t.Errorf("error = %e, want %e", err, testErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAWSCredential_ProgrammaticAuthRegionError(t *testing.T) {
|
|
||||||
tfc := testFileConfig
|
|
||||||
securityCredentials := AwsSecurityCredentials{
|
|
||||||
AccessKeyID: accessKeyID,
|
|
||||||
SecretAccessKey: secretAccessKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
testErr := errors.New("test")
|
|
||||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
|
||||||
awsRegion: "",
|
|
||||||
regionErr: testErr,
|
|
||||||
credentials: &securityCredentials,
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse() failed %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = base.subjectToken()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("subjectToken() should have failed")
|
|
||||||
}
|
|
||||||
if err != testErr {
|
|
||||||
t.Errorf("error = %e, want %e", err, testErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAWSCredential_ProgrammaticAuthOptions(t *testing.T) {
|
|
||||||
tfc := testFileConfig
|
|
||||||
securityCredentials := AwsSecurityCredentials{
|
|
||||||
AccessKeyID: accessKeyID,
|
|
||||||
SecretAccessKey: secretAccessKey,
|
|
||||||
}
|
|
||||||
expectedOptions := SupplierOptions{Audience: tfc.Audience, SubjectTokenType: tfc.SubjectTokenType}
|
|
||||||
|
|
||||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
|
||||||
awsRegion: "us-east-2",
|
|
||||||
credentials: &securityCredentials,
|
|
||||||
expectedOptions: &expectedOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse() failed %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = base.subjectToken()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("subjectToken() failed %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAWSCredential_ProgrammaticAuthContext(t *testing.T) {
|
|
||||||
tfc := testFileConfig
|
|
||||||
securityCredentials := AwsSecurityCredentials{
|
|
||||||
AccessKeyID: accessKeyID,
|
|
||||||
SecretAccessKey: secretAccessKey,
|
|
||||||
}
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
tfc.AwsSecurityCredentialsSupplier = testAwsSupplier{
|
|
||||||
awsRegion: "us-east-2",
|
|
||||||
credentials: &securityCredentials,
|
|
||||||
expectedContext: ctx,
|
|
||||||
}
|
|
||||||
|
|
||||||
base, err := tfc.parse(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parse() failed %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = base.subjectToken()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("subjectToken() failed %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAwsCredential_CredentialSourceType(t *testing.T) {
|
func TestAwsCredential_CredentialSourceType(t *testing.T) {
|
||||||
server := createDefaultAwsTestServer()
|
server := createDefaultAwsTestServer()
|
||||||
ts := httptest.NewServer(server)
|
ts := httptest.NewServer(server)
|
||||||
@@ -1437,52 +1251,3 @@ func TestAwsCredential_CredentialSourceType(t *testing.T) {
|
|||||||
t.Errorf("got %v but want %v", got, want)
|
t.Errorf("got %v but want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type testAwsSupplier struct {
|
|
||||||
err error
|
|
||||||
regionErr error
|
|
||||||
awsRegion string
|
|
||||||
credentials *AwsSecurityCredentials
|
|
||||||
expectedOptions *SupplierOptions
|
|
||||||
expectedContext context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (supp testAwsSupplier) AwsRegion(ctx context.Context, options SupplierOptions) (string, error) {
|
|
||||||
if supp.regionErr != nil {
|
|
||||||
return "", supp.regionErr
|
|
||||||
}
|
|
||||||
if supp.expectedOptions != nil {
|
|
||||||
if supp.expectedOptions.Audience != options.Audience {
|
|
||||||
return "", errors.New("Audience does not match")
|
|
||||||
}
|
|
||||||
if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType {
|
|
||||||
return "", errors.New("Audience does not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if supp.expectedContext != nil {
|
|
||||||
if supp.expectedContext != ctx {
|
|
||||||
return "", errors.New("Context does not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return supp.awsRegion, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (supp testAwsSupplier) AwsSecurityCredentials(ctx context.Context, options SupplierOptions) (*AwsSecurityCredentials, error) {
|
|
||||||
if supp.err != nil {
|
|
||||||
return nil, supp.err
|
|
||||||
}
|
|
||||||
if supp.expectedOptions != nil {
|
|
||||||
if supp.expectedOptions.Audience != options.Audience {
|
|
||||||
return nil, errors.New("Audience does not match")
|
|
||||||
}
|
|
||||||
if supp.expectedOptions.SubjectTokenType != options.SubjectTokenType {
|
|
||||||
return nil, errors.New("Audience does not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if supp.expectedContext != nil {
|
|
||||||
if supp.expectedContext != ctx {
|
|
||||||
return nil, errors.New("Context does not match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return supp.credentials, nil
|
|
||||||
}
|
|
||||||
254
google/internal/externalaccount/basecredentials.go
Normal file
254
google/internal/externalaccount/basecredentials.go
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
// Copyright 2020 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package externalaccount
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
"golang.org/x/oauth2/google/internal/stsexchange"
|
||||||
|
)
|
||||||
|
|
||||||
|
// now aliases time.Now for testing
|
||||||
|
var now = func() time.Time {
|
||||||
|
return time.Now().UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config stores the configuration for fetching tokens with external credentials.
|
||||||
|
type Config struct {
|
||||||
|
// Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
|
||||||
|
// identity pool or the workforce pool and the provider identifier in that pool.
|
||||||
|
Audience string
|
||||||
|
// SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
|
||||||
|
// e.g. `urn:ietf:params:oauth:token-type:jwt`.
|
||||||
|
SubjectTokenType string
|
||||||
|
// TokenURL is the STS token exchange endpoint.
|
||||||
|
TokenURL string
|
||||||
|
// TokenInfoURL is the token_info endpoint used to retrieve the account related information (
|
||||||
|
// user attributes like account identifier, eg. email, username, uid, etc). This is
|
||||||
|
// needed for gCloud session account identification.
|
||||||
|
TokenInfoURL string
|
||||||
|
// ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
|
||||||
|
// required for workload identity pools when APIs to be accessed have not integrated with UberMint.
|
||||||
|
ServiceAccountImpersonationURL string
|
||||||
|
// ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
|
||||||
|
// token will be valid for.
|
||||||
|
ServiceAccountImpersonationLifetimeSeconds int
|
||||||
|
// ClientSecret is currently only required if token_info endpoint also
|
||||||
|
// needs to be called with the generated GCP access token. When provided, STS will be
|
||||||
|
// called with additional basic authentication using client_id as username and client_secret as password.
|
||||||
|
ClientSecret string
|
||||||
|
// ClientID is only required in conjunction with ClientSecret, as described above.
|
||||||
|
ClientID string
|
||||||
|
// CredentialSource contains the necessary information to retrieve the token itself, as well
|
||||||
|
// as some environmental information.
|
||||||
|
CredentialSource CredentialSource
|
||||||
|
// QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
|
||||||
|
// will set the x-goog-user-project which overrides the project associated with the credentials.
|
||||||
|
QuotaProjectID string
|
||||||
|
// Scopes contains the desired scopes for the returned access token.
|
||||||
|
Scopes []string
|
||||||
|
// The optional workforce pool user project number when the credential
|
||||||
|
// corresponds to a workforce pool and not a workload identity pool.
|
||||||
|
// The underlying principal must still have serviceusage.services.use IAM
|
||||||
|
// permission to use the project for billing/quota.
|
||||||
|
WorkforcePoolUserProject string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateWorkforceAudience(input string) bool {
|
||||||
|
return validWorkforceAudiencePattern.MatchString(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return c.tokenSource(ctx, "https")
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenSource is a private function that's directly called by some of the tests,
|
||||||
|
// because the unit test URLs are mocked, and would otherwise fail the
|
||||||
|
// validity check.
|
||||||
|
func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) {
|
||||||
|
if c.WorkforcePoolUserProject != "" {
|
||||||
|
valid := validateWorkforceAudience(c.Audience)
|
||||||
|
if !valid {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := tokenSource{
|
||||||
|
ctx: ctx,
|
||||||
|
conf: c,
|
||||||
|
}
|
||||||
|
if c.ServiceAccountImpersonationURL == "" {
|
||||||
|
return oauth2.ReuseTokenSource(nil, ts), nil
|
||||||
|
}
|
||||||
|
scopes := c.Scopes
|
||||||
|
ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
|
||||||
|
imp := ImpersonateTokenSource{
|
||||||
|
Ctx: ctx,
|
||||||
|
URL: c.ServiceAccountImpersonationURL,
|
||||||
|
Scopes: scopes,
|
||||||
|
Ts: oauth2.ReuseTokenSource(nil, ts),
|
||||||
|
TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
|
||||||
|
}
|
||||||
|
return oauth2.ReuseTokenSource(nil, imp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject token file types.
|
||||||
|
const (
|
||||||
|
fileTypeText = "text"
|
||||||
|
fileTypeJSON = "json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type format struct {
|
||||||
|
// Type is either "text" or "json". When not provided "text" type is assumed.
|
||||||
|
Type string `json:"type"`
|
||||||
|
// SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
|
||||||
|
SubjectTokenFieldName string `json:"subject_token_field_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
|
||||||
|
// One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question.
|
||||||
|
// The EnvironmentID should start with AWS if being used for an AWS credential.
|
||||||
|
type CredentialSource struct {
|
||||||
|
File string `json:"file"`
|
||||||
|
|
||||||
|
URL string `json:"url"`
|
||||||
|
Headers map[string]string `json:"headers"`
|
||||||
|
|
||||||
|
Executable *ExecutableConfig `json:"executable"`
|
||||||
|
|
||||||
|
EnvironmentID string `json:"environment_id"`
|
||||||
|
RegionURL string `json:"region_url"`
|
||||||
|
RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
|
||||||
|
CredVerificationURL string `json:"cred_verification_url"`
|
||||||
|
IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
|
||||||
|
Format format `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecutableConfig struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
TimeoutMillis *int `json:"timeout_millis"`
|
||||||
|
OutputFile string `json:"output_file"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse determines the type of CredentialSource needed.
|
||||||
|
func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
|
||||||
|
if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
|
||||||
|
if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
|
||||||
|
if awsVersion != 1 {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
awsCredSource := awsCredentialSource{
|
||||||
|
EnvironmentID: c.CredentialSource.EnvironmentID,
|
||||||
|
RegionURL: c.CredentialSource.RegionURL,
|
||||||
|
RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
|
||||||
|
CredVerificationURL: c.CredentialSource.URL,
|
||||||
|
TargetResource: c.Audience,
|
||||||
|
ctx: ctx,
|
||||||
|
}
|
||||||
|
if c.CredentialSource.IMDSv2SessionTokenURL != "" {
|
||||||
|
awsCredSource.IMDSv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return awsCredSource, nil
|
||||||
|
}
|
||||||
|
} else if c.CredentialSource.File != "" {
|
||||||
|
return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
|
||||||
|
} else if c.CredentialSource.URL != "" {
|
||||||
|
return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
|
||||||
|
} else if c.CredentialSource.Executable != nil {
|
||||||
|
return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseCredentialSource interface {
|
||||||
|
credentialSourceType() string
|
||||||
|
subjectToken() (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
|
||||||
|
type tokenSource struct {
|
||||||
|
ctx context.Context
|
||||||
|
conf *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
|
||||||
|
return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
|
||||||
|
goVersion(),
|
||||||
|
"unknown",
|
||||||
|
credSource.credentialSourceType(),
|
||||||
|
conf.ServiceAccountImpersonationURL != "",
|
||||||
|
conf.ServiceAccountImpersonationLifetimeSeconds != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token allows tokenSource to conform to the oauth2.TokenSource interface.
|
||||||
|
func (ts tokenSource) Token() (*oauth2.Token, error) {
|
||||||
|
conf := ts.conf
|
||||||
|
|
||||||
|
credSource, err := conf.parse(ts.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
subjectToken, err := credSource.subjectToken()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
stsRequest := stsexchange.TokenExchangeRequest{
|
||||||
|
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
|
||||||
|
Audience: conf.Audience,
|
||||||
|
Scope: conf.Scopes,
|
||||||
|
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
|
||||||
|
SubjectToken: subjectToken,
|
||||||
|
SubjectTokenType: conf.SubjectTokenType,
|
||||||
|
}
|
||||||
|
header := make(http.Header)
|
||||||
|
header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
|
||||||
|
clientAuth := stsexchange.ClientAuthentication{
|
||||||
|
AuthStyle: oauth2.AuthStyleInHeader,
|
||||||
|
ClientID: conf.ClientID,
|
||||||
|
ClientSecret: conf.ClientSecret,
|
||||||
|
}
|
||||||
|
var options map[string]interface{}
|
||||||
|
// Do not pass workforce_pool_user_project when client authentication is used.
|
||||||
|
// The client ID is sufficient for determining the user project.
|
||||||
|
if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
|
||||||
|
options = map[string]interface{}{
|
||||||
|
"userProject": conf.WorkforcePoolUserProject,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessToken := &oauth2.Token{
|
||||||
|
AccessToken: stsResp.AccessToken,
|
||||||
|
TokenType: stsResp.TokenType,
|
||||||
|
}
|
||||||
|
if stsResp.ExpiresIn < 0 {
|
||||||
|
return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service")
|
||||||
|
} else if stsResp.ExpiresIn >= 0 {
|
||||||
|
accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stsResp.RefreshToken != "" {
|
||||||
|
accessToken.RefreshToken = stsResp.RefreshToken
|
||||||
|
}
|
||||||
|
return accessToken, nil
|
||||||
|
}
|
||||||
259
google/internal/externalaccount/basecredentials_test.go
Normal file
259
google/internal/externalaccount/basecredentials_test.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
// Copyright 2020 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package externalaccount
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
textBaseCredPath = "testdata/3pi_cred.txt"
|
||||||
|
jsonBaseCredPath = "testdata/3pi_cred.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testBaseCredSource = CredentialSource{
|
||||||
|
File: textBaseCredPath,
|
||||||
|
Format: format{Type: fileTypeText},
|
||||||
|
}
|
||||||
|
|
||||||
|
var testConfig = Config{
|
||||||
|
Audience: "32555940559.apps.googleusercontent.com",
|
||||||
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||||
|
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||||
|
ClientSecret: "notsosecret",
|
||||||
|
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||||
|
CredentialSource: testBaseCredSource,
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseCredsRequestBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||||
|
baseCredsResponseBody = `{"access_token":"Sample.Access.Token","issued_token_type":"urn:ietf:params:oauth:token-type:access_token","token_type":"Bearer","expires_in":3600,"scope":"https://www.googleapis.com/auth/cloud-platform"}`
|
||||||
|
workforcePoolRequestBodyWithClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||||
|
workforcePoolRequestBodyWithoutClientId = "audience=%2F%2Fiam.googleapis.com%2Flocations%2Feu%2FworkforcePools%2Fpool-id%2Fproviders%2Fprovider-id&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&options=%7B%22userProject%22%3A%22myProject%22%7D&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aid_token"
|
||||||
|
correctAT = "Sample.Access.Token"
|
||||||
|
expiry int64 = 234852
|
||||||
|
)
|
||||||
|
var (
|
||||||
|
testNow = func() time.Time { return time.Unix(expiry, 0) }
|
||||||
|
)
|
||||||
|
|
||||||
|
type testExchangeTokenServer struct {
|
||||||
|
url string
|
||||||
|
authorization string
|
||||||
|
contentType string
|
||||||
|
metricsHeader string
|
||||||
|
body string
|
||||||
|
response string
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(t *testing.T, config *Config, tets *testExchangeTokenServer) (*oauth2.Token, error) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got, want := r.URL.String(), tets.url; got != want {
|
||||||
|
t.Errorf("URL.String(): got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerAuth := r.Header.Get("Authorization")
|
||||||
|
if got, want := headerAuth, tets.authorization; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerContentType := r.Header.Get("Content-Type")
|
||||||
|
if got, want := headerContentType, tets.contentType; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerMetrics := r.Header.Get("x-goog-api-client")
|
||||||
|
if got, want := headerMetrics, tets.metricsHeader; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed reading request body: %s.", err)
|
||||||
|
}
|
||||||
|
if got, want := string(body), tets.body; got != want {
|
||||||
|
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(tets.response))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
config.TokenURL = server.URL
|
||||||
|
|
||||||
|
oldNow := now
|
||||||
|
defer func() { now = oldNow }()
|
||||||
|
now = testNow
|
||||||
|
|
||||||
|
ts := tokenSource{
|
||||||
|
ctx: context.Background(),
|
||||||
|
conf: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ts.Token()
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateToken(t *testing.T, tok *oauth2.Token) {
|
||||||
|
if got, want := tok.AccessToken, correctAT; got != want {
|
||||||
|
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := tok.TokenType, "Bearer"; got != want {
|
||||||
|
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got, want := tok.Expiry, testNow().Add(time.Duration(3600)*time.Second); got != want {
|
||||||
|
t.Errorf("Unexpected Expiry: got %v, but wanted %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getExpectedMetricsHeader(source string, saImpersonation bool, configLifetime bool) string {
|
||||||
|
return fmt.Sprintf("gl-go/%s auth/unknown google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t", goVersion(), source, saImpersonation, configLifetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToken(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Audience: "32555940559.apps.googleusercontent.com",
|
||||||
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||||
|
ClientSecret: "notsosecret",
|
||||||
|
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||||
|
CredentialSource: testBaseCredSource,
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||||
|
}
|
||||||
|
|
||||||
|
server := testExchangeTokenServer{
|
||||||
|
url: "/",
|
||||||
|
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
||||||
|
contentType: "application/x-www-form-urlencoded",
|
||||||
|
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||||
|
body: baseCredsRequestBody,
|
||||||
|
response: baseCredsResponseBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := run(t, &config, &server)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %e", err)
|
||||||
|
}
|
||||||
|
validateToken(t, tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkforcePoolTokenWithClientID(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
||||||
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||||
|
ClientSecret: "notsosecret",
|
||||||
|
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||||
|
CredentialSource: testBaseCredSource,
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||||
|
WorkforcePoolUserProject: "myProject",
|
||||||
|
}
|
||||||
|
|
||||||
|
server := testExchangeTokenServer{
|
||||||
|
url: "/",
|
||||||
|
authorization: "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ=",
|
||||||
|
contentType: "application/x-www-form-urlencoded",
|
||||||
|
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||||
|
body: workforcePoolRequestBodyWithClientId,
|
||||||
|
response: baseCredsResponseBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := run(t, &config, &server)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %e", err)
|
||||||
|
}
|
||||||
|
validateToken(t, tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkforcePoolTokenWithoutClientID(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Audience: "//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id",
|
||||||
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||||
|
ClientSecret: "notsosecret",
|
||||||
|
CredentialSource: testBaseCredSource,
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||||
|
WorkforcePoolUserProject: "myProject",
|
||||||
|
}
|
||||||
|
|
||||||
|
server := testExchangeTokenServer{
|
||||||
|
url: "/",
|
||||||
|
authorization: "",
|
||||||
|
contentType: "application/x-www-form-urlencoded",
|
||||||
|
metricsHeader: getExpectedMetricsHeader("file", false, false),
|
||||||
|
body: workforcePoolRequestBodyWithoutClientId,
|
||||||
|
response: baseCredsResponseBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := run(t, &config, &server)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %e", err)
|
||||||
|
}
|
||||||
|
validateToken(t, tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonworkforceWithWorkforcePoolUserProject(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
Audience: "32555940559.apps.googleusercontent.com",
|
||||||
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
|
||||||
|
TokenURL: "https://sts.googleapis.com",
|
||||||
|
ClientSecret: "notsosecret",
|
||||||
|
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||||
|
CredentialSource: testBaseCredSource,
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||||
|
WorkforcePoolUserProject: "myProject",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := config.TokenSource(context.Background())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error but found none")
|
||||||
|
}
|
||||||
|
if got, want := err.Error(), "oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials"; got != want {
|
||||||
|
t.Errorf("Incorrect error received.\nExpected: %s\nRecieved: %s", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkforcePoolCreation(t *testing.T) {
|
||||||
|
var audienceValidatyTests = []struct {
|
||||||
|
audience string
|
||||||
|
expectSuccess bool
|
||||||
|
}{
|
||||||
|
{"//iam.googleapis.com/locations/global/workforcePools/pool-id/providers/provider-id", true},
|
||||||
|
{"//iam.googleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", true},
|
||||||
|
{"//iam.googleapis.com/locations/eu/workforcePools/workloadIdentityPools/providers/provider-id", true},
|
||||||
|
{"identitynamespace:1f12345:my_provider", false},
|
||||||
|
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/pool-id/providers/provider-id", false},
|
||||||
|
{"//iam.googleapis.com/projects/123456/locations/eu/workloadIdentityPools/pool-id/providers/provider-id", false},
|
||||||
|
{"//iam.googleapis.com/projects/123456/locations/global/workloadIdentityPools/workforcePools/providers/provider-id", false},
|
||||||
|
{"//iamgoogleapis.com/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
||||||
|
{"//iam.googleapiscom/locations/eu/workforcePools/pool-id/providers/provider-id", false},
|
||||||
|
{"//iam.googleapis.com/locations/workforcePools/pool-id/providers/provider-id", false},
|
||||||
|
{"//iam.googleapis.com/locations/eu/workforcePool/pool-id/providers/provider-id", false},
|
||||||
|
{"//iam.googleapis.com/locations//workforcePool/pool-id/providers/provider-id", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
for _, tt := range audienceValidatyTests {
|
||||||
|
t.Run(" "+tt.audience, 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 = "https://iamcredentials.googleapis.com"
|
||||||
|
config.Audience = tt.audience
|
||||||
|
config.WorkforcePoolUserProject = "myProject"
|
||||||
|
_, 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
18
google/internal/externalaccount/err.go
Normal file
18
google/internal/externalaccount/err.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2020 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package externalaccount
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// Error for handling OAuth related error responses as stated in rfc6749#5.2.
|
||||||
|
type Error struct {
|
||||||
|
Code string
|
||||||
|
URI string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *Error) Error() string {
|
||||||
|
return fmt.Sprintf("got error code %s from %s: %s", err.Code, err.URI, err.Description)
|
||||||
|
}
|
||||||
19
google/internal/externalaccount/err_test.go
Normal file
19
google/internal/externalaccount/err_test.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2020 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package externalaccount
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestError(t *testing.T) {
|
||||||
|
e := Error{
|
||||||
|
"42",
|
||||||
|
"http:thisIsAPlaceholder",
|
||||||
|
"The Answer!",
|
||||||
|
}
|
||||||
|
want := "got error code 42 from http:thisIsAPlaceholder: The Answer!"
|
||||||
|
if got := e.Error(); got != want {
|
||||||
|
t.Errorf("Got error message %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,51 +39,51 @@ func (nce nonCacheableError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func missingFieldError(source, field string) error {
|
func missingFieldError(source, field string) error {
|
||||||
return fmt.Errorf("oauth2/google/externalaccount: %v missing `%q` field", source, field)
|
return fmt.Errorf("oauth2/google: %v missing `%q` field", source, field)
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonParsingError(source, data string) error {
|
func jsonParsingError(source, data string) error {
|
||||||
return fmt.Errorf("oauth2/google/externalaccount: unable to parse %v\nResponse: %v", source, data)
|
return fmt.Errorf("oauth2/google: unable to parse %v\nResponse: %v", source, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func malformedFailureError() error {
|
func malformedFailureError() error {
|
||||||
return nonCacheableError{"oauth2/google/externalaccount: response must include `error` and `message` fields when unsuccessful"}
|
return nonCacheableError{"oauth2/google: response must include `error` and `message` fields when unsuccessful"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func userDefinedError(code, message string) error {
|
func userDefinedError(code, message string) error {
|
||||||
return nonCacheableError{fmt.Sprintf("oauth2/google/externalaccount: response contains unsuccessful response: (%v) %v", code, message)}
|
return nonCacheableError{fmt.Sprintf("oauth2/google: response contains unsuccessful response: (%v) %v", code, message)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsupportedVersionError(source string, version int) error {
|
func unsupportedVersionError(source string, version int) error {
|
||||||
return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported version: %v", source, version)
|
return fmt.Errorf("oauth2/google: %v contains unsupported version: %v", source, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenExpiredError() error {
|
func tokenExpiredError() error {
|
||||||
return nonCacheableError{"oauth2/google/externalaccount: the token returned by the executable is expired"}
|
return nonCacheableError{"oauth2/google: the token returned by the executable is expired"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func tokenTypeError(source string) error {
|
func tokenTypeError(source string) error {
|
||||||
return fmt.Errorf("oauth2/google/externalaccount: %v contains unsupported token type", source)
|
return fmt.Errorf("oauth2/google: %v contains unsupported token type", source)
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitCodeError(exitCode int) error {
|
func exitCodeError(exitCode int) error {
|
||||||
return fmt.Errorf("oauth2/google/externalaccount: executable command failed with exit code %v", exitCode)
|
return fmt.Errorf("oauth2/google: executable command failed with exit code %v", exitCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func executableError(err error) error {
|
func executableError(err error) error {
|
||||||
return fmt.Errorf("oauth2/google/externalaccount: executable command failed: %v", err)
|
return fmt.Errorf("oauth2/google: executable command failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func executablesDisallowedError() error {
|
func executablesDisallowedError() error {
|
||||||
return errors.New("oauth2/google/externalaccount: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
|
return errors.New("oauth2/google: executables need to be explicitly allowed (set GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES to '1') to run")
|
||||||
}
|
}
|
||||||
|
|
||||||
func timeoutRangeError() error {
|
func timeoutRangeError() error {
|
||||||
return errors.New("oauth2/google/externalaccount: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds")
|
return errors.New("oauth2/google: invalid `timeout_millis` field — executable timeout must be between 5 and 120 seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandMissingError() error {
|
func commandMissingError() error {
|
||||||
return errors.New("oauth2/google/externalaccount: missing `command` field — executable command must be provided")
|
return errors.New("oauth2/google: missing `command` field — executable command must be provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
type environment interface {
|
type environment interface {
|
||||||
@@ -146,7 +146,7 @@ type executableCredentialSource struct {
|
|||||||
|
|
||||||
// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
|
// CreateExecutableCredential creates an executableCredentialSource given an ExecutableConfig.
|
||||||
// It also performs defaulting and type conversions.
|
// It also performs defaulting and type conversions.
|
||||||
func createExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) {
|
func CreateExecutableCredential(ctx context.Context, ec *ExecutableConfig, config *Config) (executableCredentialSource, error) {
|
||||||
if ec.Command == "" {
|
if ec.Command == "" {
|
||||||
return executableCredentialSource{}, commandMissingError()
|
return executableCredentialSource{}, commandMissingError()
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@ var creationTests = []struct {
|
|||||||
func TestCreateExecutableCredential(t *testing.T) {
|
func TestCreateExecutableCredential(t *testing.T) {
|
||||||
for _, tt := range creationTests {
|
for _, tt := range creationTests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ecs, err := createExecutableCredential(context.Background(), &tt.executableConfig, nil)
|
ecs, err := CreateExecutableCredential(context.Background(), &tt.executableConfig, nil)
|
||||||
if tt.expectedErr != nil {
|
if tt.expectedErr != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatalf("Expected error but found none")
|
t.Fatalf("Expected error but found none")
|
||||||
@@ -169,7 +169,7 @@ var getEnvironmentTests = []struct {
|
|||||||
config: Config{
|
config: Config{
|
||||||
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||||
CredentialSource: &CredentialSource{
|
CredentialSource: CredentialSource{
|
||||||
Executable: &ExecutableConfig{
|
Executable: &ExecutableConfig{
|
||||||
Command: "blarg",
|
Command: "blarg",
|
||||||
},
|
},
|
||||||
@@ -193,7 +193,7 @@ var getEnvironmentTests = []struct {
|
|||||||
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
||||||
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
|
ServiceAccountImpersonationURL: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken",
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||||
CredentialSource: &CredentialSource{
|
CredentialSource: CredentialSource{
|
||||||
Executable: &ExecutableConfig{
|
Executable: &ExecutableConfig{
|
||||||
Command: "blarg",
|
Command: "blarg",
|
||||||
OutputFile: "/path/to/generated/cached/credentials",
|
OutputFile: "/path/to/generated/cached/credentials",
|
||||||
@@ -220,7 +220,7 @@ var getEnvironmentTests = []struct {
|
|||||||
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
Audience: "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/oidc",
|
||||||
ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com",
|
ServiceAccountImpersonationURL: "test@project.iam.gserviceaccount.com",
|
||||||
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||||
CredentialSource: &CredentialSource{
|
CredentialSource: CredentialSource{
|
||||||
Executable: &ExecutableConfig{
|
Executable: &ExecutableConfig{
|
||||||
Command: "blarg",
|
Command: "blarg",
|
||||||
OutputFile: "/path/to/generated/cached/credentials",
|
OutputFile: "/path/to/generated/cached/credentials",
|
||||||
@@ -247,7 +247,7 @@ func TestExecutableCredentialGetEnvironment(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
config := tt.config
|
config := tt.config
|
||||||
|
|
||||||
ecs, err := createExecutableCredential(context.Background(), config.CredentialSource.Executable, &config)
|
ecs, err := CreateExecutableCredential(context.Background(), config.CredentialSource.Executable, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("creation failed %v", err)
|
t.Fatalf("creation failed %v", err)
|
||||||
}
|
}
|
||||||
@@ -471,7 +471,7 @@ func TestRetrieveExecutableSubjectTokenExecutableErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -578,7 +578,7 @@ func TestRetrieveExecutableSubjectTokenSuccesses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -629,7 +629,7 @@ func TestRetrieveOutputFileSubjectTokenNotJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -778,7 +778,7 @@ func TestRetrieveOutputFileSubjectTokenFailureTests(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -881,7 +881,7 @@ func TestRetrieveOutputFileSubjectTokenInvalidCache(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -986,7 +986,7 @@ func TestRetrieveOutputFileSubjectTokenJwt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
type fileCredentialSource struct {
|
type fileCredentialSource struct {
|
||||||
File string
|
File string
|
||||||
Format Format
|
Format format
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs fileCredentialSource) credentialSourceType() string {
|
func (cs fileCredentialSource) credentialSourceType() string {
|
||||||
@@ -26,12 +26,12 @@ func (cs fileCredentialSource) credentialSourceType() string {
|
|||||||
func (cs fileCredentialSource) subjectToken() (string, error) {
|
func (cs fileCredentialSource) subjectToken() (string, error) {
|
||||||
tokenFile, err := os.Open(cs.File)
|
tokenFile, err := os.Open(cs.File)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to open credential file %q", cs.File)
|
return "", fmt.Errorf("oauth2/google: failed to open credential file %q", cs.File)
|
||||||
}
|
}
|
||||||
defer tokenFile.Close()
|
defer tokenFile.Close()
|
||||||
tokenBytes, err := ioutil.ReadAll(io.LimitReader(tokenFile, 1<<20))
|
tokenBytes, err := ioutil.ReadAll(io.LimitReader(tokenFile, 1<<20))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to read credential file: %v", err)
|
return "", fmt.Errorf("oauth2/google: failed to read credential file: %v", err)
|
||||||
}
|
}
|
||||||
tokenBytes = bytes.TrimSpace(tokenBytes)
|
tokenBytes = bytes.TrimSpace(tokenBytes)
|
||||||
switch cs.Format.Type {
|
switch cs.Format.Type {
|
||||||
@@ -39,15 +39,15 @@ func (cs fileCredentialSource) subjectToken() (string, error) {
|
|||||||
jsonData := make(map[string]interface{})
|
jsonData := make(map[string]interface{})
|
||||||
err = json.Unmarshal(tokenBytes, &jsonData)
|
err = json.Unmarshal(tokenBytes, &jsonData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err)
|
return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err)
|
||||||
}
|
}
|
||||||
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials")
|
return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials")
|
||||||
}
|
}
|
||||||
token, ok := val.(string)
|
token, ok := val.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token")
|
return "", errors.New("oauth2/google: improperly formatted subject token")
|
||||||
}
|
}
|
||||||
return token, nil
|
return token, nil
|
||||||
case "text":
|
case "text":
|
||||||
@@ -55,7 +55,7 @@ func (cs fileCredentialSource) subjectToken() (string, error) {
|
|||||||
case "":
|
case "":
|
||||||
return string(tokenBytes), nil
|
return string(tokenBytes), nil
|
||||||
default:
|
default:
|
||||||
return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type")
|
return "", errors.New("oauth2/google: invalid credential_source file format type")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
|
|||||||
name: "TextFileSource",
|
name: "TextFileSource",
|
||||||
cs: CredentialSource{
|
cs: CredentialSource{
|
||||||
File: textBaseCredPath,
|
File: textBaseCredPath,
|
||||||
Format: Format{Type: fileTypeText},
|
Format: format{Type: fileTypeText},
|
||||||
},
|
},
|
||||||
want: "street123",
|
want: "street123",
|
||||||
},
|
},
|
||||||
@@ -44,7 +44,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
|
|||||||
name: "JSONFileSource",
|
name: "JSONFileSource",
|
||||||
cs: CredentialSource{
|
cs: CredentialSource{
|
||||||
File: jsonBaseCredPath,
|
File: jsonBaseCredPath,
|
||||||
Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
||||||
},
|
},
|
||||||
want: "321road",
|
want: "321road",
|
||||||
},
|
},
|
||||||
@@ -53,7 +53,7 @@ func TestRetrieveFileSubjectToken(t *testing.T) {
|
|||||||
for _, test := range fileSourceTests {
|
for _, test := range fileSourceTests {
|
||||||
test := test
|
test := test
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &test.cs
|
tfc.CredentialSource = test.cs
|
||||||
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package impersonate
|
package externalaccount
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
144
google/internal/externalaccount/impersonate_test.go
Normal file
144
google/internal/externalaccount/impersonate_test.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright 2021 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package externalaccount
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseImpersonateCredsReqBody = "audience=32555940559.apps.googleusercontent.com&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Atoken-exchange&requested_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Aaccess_token&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&subject_token=street123&subject_token_type=urn%3Aietf%3Aparams%3Aoauth%3Atoken-type%3Ajwt"
|
||||||
|
baseImpersonateCredsRespBody = `{"accessToken":"Second.Access.Token","expireTime":"2020-12-28T15:01:23Z"}`
|
||||||
|
)
|
||||||
|
|
||||||
|
func createImpersonationServer(urlWanted, authWanted, bodyWanted, response string, t *testing.T) *httptest.Server {
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got, want := r.URL.String(), urlWanted; got != want {
|
||||||
|
t.Errorf("URL.String(): got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerAuth := r.Header.Get("Authorization")
|
||||||
|
if got, want := headerAuth, authWanted; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerContentType := r.Header.Get("Content-Type")
|
||||||
|
if got, want := headerContentType, "application/json"; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed reading request body: %v.", err)
|
||||||
|
}
|
||||||
|
if got, want := string(body), bodyWanted; got != want {
|
||||||
|
t.Errorf("Unexpected impersonation payload: got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(response))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTargetServer(metricsHeaderWanted string, t *testing.T) *httptest.Server {
|
||||||
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if got, want := r.URL.String(), "/"; got != want {
|
||||||
|
t.Errorf("URL.String(): got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerAuth := r.Header.Get("Authorization")
|
||||||
|
if got, want := headerAuth, "Basic cmJyZ25vZ25yaG9uZ28zYmk0Z2I5Z2hnOWc6bm90c29zZWNyZXQ="; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerContentType := r.Header.Get("Content-Type")
|
||||||
|
if got, want := headerContentType, "application/x-www-form-urlencoded"; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
headerMetrics := r.Header.Get("x-goog-api-client")
|
||||||
|
if got, want := headerMetrics, metricsHeaderWanted; got != want {
|
||||||
|
t.Errorf("got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed reading request body: %v.", err)
|
||||||
|
}
|
||||||
|
if got, want := string(body), baseImpersonateCredsReqBody; got != want {
|
||||||
|
t.Errorf("Unexpected exchange payload: got %v but want %v", got, want)
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(baseCredsResponseBody))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
var impersonationTests = []struct {
|
||||||
|
name string
|
||||||
|
config Config
|
||||||
|
expectedImpersonationBody string
|
||||||
|
expectedMetricsHeader string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Base Impersonation",
|
||||||
|
config: Config{
|
||||||
|
Audience: "32555940559.apps.googleusercontent.com",
|
||||||
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||||
|
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||||
|
ClientSecret: "notsosecret",
|
||||||
|
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||||
|
CredentialSource: testBaseCredSource,
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||||
|
},
|
||||||
|
expectedImpersonationBody: "{\"lifetime\":\"3600s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
|
||||||
|
expectedMetricsHeader: getExpectedMetricsHeader("file", true, false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "With TokenLifetime Set",
|
||||||
|
config: Config{
|
||||||
|
Audience: "32555940559.apps.googleusercontent.com",
|
||||||
|
SubjectTokenType: "urn:ietf:params:oauth:token-type:jwt",
|
||||||
|
TokenInfoURL: "http://localhost:8080/v1/tokeninfo",
|
||||||
|
ClientSecret: "notsosecret",
|
||||||
|
ClientID: "rbrgnognrhongo3bi4gb9ghg9g",
|
||||||
|
CredentialSource: testBaseCredSource,
|
||||||
|
Scopes: []string{"https://www.googleapis.com/auth/devstorage.full_control"},
|
||||||
|
ServiceAccountImpersonationLifetimeSeconds: 10000,
|
||||||
|
},
|
||||||
|
expectedImpersonationBody: "{\"lifetime\":\"10000s\",\"scope\":[\"https://www.googleapis.com/auth/devstorage.full_control\"]}",
|
||||||
|
expectedMetricsHeader: getExpectedMetricsHeader("file", true, true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImpersonation(t *testing.T) {
|
||||||
|
for _, tt := range impersonationTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
testImpersonateConfig := tt.config
|
||||||
|
impersonateServer := createImpersonationServer("/", "Bearer Sample.Access.Token", tt.expectedImpersonationBody, baseImpersonateCredsRespBody, t)
|
||||||
|
defer impersonateServer.Close()
|
||||||
|
testImpersonateConfig.ServiceAccountImpersonationURL = impersonateServer.URL
|
||||||
|
|
||||||
|
targetServer := createTargetServer(tt.expectedMetricsHeader, t)
|
||||||
|
defer targetServer.Close()
|
||||||
|
testImpersonateConfig.TokenURL = targetServer.URL
|
||||||
|
|
||||||
|
ourTS, err := testImpersonateConfig.tokenSource(context.Background(), "http")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create TokenSource: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldNow := now
|
||||||
|
defer func() { now = oldNow }()
|
||||||
|
now = testNow
|
||||||
|
|
||||||
|
tok, err := ourTS.Token()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %e", err)
|
||||||
|
}
|
||||||
|
if got, want := tok.AccessToken, "Second.Access.Token"; got != want {
|
||||||
|
t.Errorf("Unexpected access token: got %v, but wanted %v", got, want)
|
||||||
|
}
|
||||||
|
if got, want := tok.TokenType, "Bearer"; got != want {
|
||||||
|
t.Errorf("Unexpected TokenType: got %v, but wanted %v", got, want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
type urlCredentialSource struct {
|
type urlCredentialSource struct {
|
||||||
URL string
|
URL string
|
||||||
Headers map[string]string
|
Headers map[string]string
|
||||||
Format Format
|
Format format
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ func (cs urlCredentialSource) subjectToken() (string, error) {
|
|||||||
client := oauth2.NewClient(cs.ctx, nil)
|
client := oauth2.NewClient(cs.ctx, nil)
|
||||||
req, err := http.NewRequest("GET", cs.URL, nil)
|
req, err := http.NewRequest("GET", cs.URL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: HTTP request for URL-sourced credential failed: %v", err)
|
return "", fmt.Errorf("oauth2/google: HTTP request for URL-sourced credential failed: %v", err)
|
||||||
}
|
}
|
||||||
req = req.WithContext(cs.ctx)
|
req = req.WithContext(cs.ctx)
|
||||||
|
|
||||||
@@ -40,16 +40,16 @@ func (cs urlCredentialSource) subjectToken() (string, error) {
|
|||||||
}
|
}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: invalid response when retrieving subject token: %v", err)
|
return "", fmt.Errorf("oauth2/google: invalid response when retrieving subject token: %v", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
respBody, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: invalid body in subject token URL query: %v", err)
|
return "", fmt.Errorf("oauth2/google: invalid body in subject token URL query: %v", err)
|
||||||
}
|
}
|
||||||
if c := resp.StatusCode; c < 200 || c > 299 {
|
if c := resp.StatusCode; c < 200 || c > 299 {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: status code %d: %s", c, respBody)
|
return "", fmt.Errorf("oauth2/google: status code %d: %s", c, respBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch cs.Format.Type {
|
switch cs.Format.Type {
|
||||||
@@ -57,15 +57,15 @@ func (cs urlCredentialSource) subjectToken() (string, error) {
|
|||||||
jsonData := make(map[string]interface{})
|
jsonData := make(map[string]interface{})
|
||||||
err = json.Unmarshal(respBody, &jsonData)
|
err = json.Unmarshal(respBody, &jsonData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("oauth2/google/externalaccount: failed to unmarshal subject token file: %v", err)
|
return "", fmt.Errorf("oauth2/google: failed to unmarshal subject token file: %v", err)
|
||||||
}
|
}
|
||||||
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
val, ok := jsonData[cs.Format.SubjectTokenFieldName]
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("oauth2/google/externalaccount: provided subject_token_field_name not found in credentials")
|
return "", errors.New("oauth2/google: provided subject_token_field_name not found in credentials")
|
||||||
}
|
}
|
||||||
token, ok := val.(string)
|
token, ok := val.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
return "", errors.New("oauth2/google/externalaccount: improperly formatted subject token")
|
return "", errors.New("oauth2/google: improperly formatted subject token")
|
||||||
}
|
}
|
||||||
return token, nil
|
return token, nil
|
||||||
case "text":
|
case "text":
|
||||||
@@ -73,7 +73,7 @@ func (cs urlCredentialSource) subjectToken() (string, error) {
|
|||||||
case "":
|
case "":
|
||||||
return string(respBody), nil
|
return string(respBody), nil
|
||||||
default:
|
default:
|
||||||
return "", errors.New("oauth2/google/externalaccount: invalid credential_source file format type")
|
return "", errors.New("oauth2/google: invalid credential_source file format type")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -28,11 +28,11 @@ func TestRetrieveURLSubjectToken_Text(t *testing.T) {
|
|||||||
heads["Metadata"] = "True"
|
heads["Metadata"] = "True"
|
||||||
cs := CredentialSource{
|
cs := CredentialSource{
|
||||||
URL: ts.URL,
|
URL: ts.URL,
|
||||||
Format: Format{Type: fileTypeText},
|
Format: format{Type: fileTypeText},
|
||||||
Headers: heads,
|
Headers: heads,
|
||||||
}
|
}
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -60,7 +60,7 @@ func TestRetrieveURLSubjectToken_Untyped(t *testing.T) {
|
|||||||
URL: ts.URL,
|
URL: ts.URL,
|
||||||
}
|
}
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -93,10 +93,10 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
cs := CredentialSource{
|
cs := CredentialSource{
|
||||||
URL: ts.URL,
|
URL: ts.URL,
|
||||||
Format: Format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
Format: format{Type: fileTypeJSON, SubjectTokenFieldName: "SubjToken"},
|
||||||
}
|
}
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,10 +115,10 @@ func TestRetrieveURLSubjectToken_JSON(t *testing.T) {
|
|||||||
func TestURLCredential_CredentialSourceType(t *testing.T) {
|
func TestURLCredential_CredentialSourceType(t *testing.T) {
|
||||||
cs := CredentialSource{
|
cs := CredentialSource{
|
||||||
URL: "http://example.com",
|
URL: "http://example.com",
|
||||||
Format: Format{Type: fileTypeText},
|
Format: format{Type: fileTypeText},
|
||||||
}
|
}
|
||||||
tfc := testFileConfig
|
tfc := testFileConfig
|
||||||
tfc.CredentialSource = &cs
|
tfc.CredentialSource = cs
|
||||||
|
|
||||||
base, err := tfc.parse(context.Background())
|
base, err := tfc.parse(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Reference in New Issue
Block a user