123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- // 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
- }
|