// 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" "golang.org/x/oauth2" "net/http" "strconv" "time" ) // 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 // 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 } // 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 { ts := tokenSource{ ctx: ctx, conf: c, } if c.ServiceAccountImpersonationURL == "" { return oauth2.ReuseTokenSource(nil, ts) } 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), } return oauth2.ReuseTokenSource(nil, imp) } // 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. // Either the File or the URL field 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"` EnvironmentID string `json:"environment_id"` RegionURL string `json:"region_url"` RegionalCredVerificationURL string `json:"regional_cred_verification_url"` CredVerificationURL string `json:"cred_verification_url"` Format format `json:"format"` } // 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) } return awsCredentialSource{ EnvironmentID: c.CredentialSource.EnvironmentID, RegionURL: c.CredentialSource.RegionURL, RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, CredVerificationURL: c.CredentialSource.URL, TargetResource: c.Audience, ctx: ctx, }, 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 } return nil, fmt.Errorf("oauth2/google: unable to parse credential source") } type baseCredentialSource interface { 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 } // 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 := stsTokenExchangeRequest{ 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") clientAuth := clientAuthentication{ AuthStyle: oauth2.AuthStyleInHeader, ClientID: conf.ClientID, ClientSecret: conf.ClientSecret, } stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil) 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 }