1// Copyright 2020 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package externalaccount 6 7import ( 8 "context" 9 "fmt" 10 "golang.org/x/oauth2" 11 "net/http" 12 "strconv" 13 "time" 14) 15 16// now aliases time.Now for testing 17var now = func() time.Time { 18 return time.Now().UTC() 19} 20 21// Config stores the configuration for fetching tokens with external credentials. 22type Config struct { 23 Audience string 24 SubjectTokenType string 25 TokenURL string 26 TokenInfoURL string 27 ServiceAccountImpersonationURL string 28 ClientSecret string 29 ClientID string 30 CredentialSource CredentialSource 31 QuotaProjectID string 32 Scopes []string 33} 34 35// TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials. 36func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource { 37 ts := tokenSource{ 38 ctx: ctx, 39 conf: c, 40 } 41 if c.ServiceAccountImpersonationURL == "" { 42 return oauth2.ReuseTokenSource(nil, ts) 43 } 44 scopes := c.Scopes 45 ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"} 46 imp := impersonateTokenSource{ 47 ctx: ctx, 48 url: c.ServiceAccountImpersonationURL, 49 scopes: scopes, 50 ts: oauth2.ReuseTokenSource(nil, ts), 51 } 52 return oauth2.ReuseTokenSource(nil, imp) 53} 54 55// Subject token file types. 56const ( 57 fileTypeText = "text" 58 fileTypeJSON = "json" 59) 60 61type format struct { 62 // Type is either "text" or "json". When not provided "text" type is assumed. 63 Type string `json:"type"` 64 // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure. 65 SubjectTokenFieldName string `json:"subject_token_field_name"` 66} 67 68// CredentialSource stores the information necessary to retrieve the credentials for the STS exchange. 69type CredentialSource struct { 70 File string `json:"file"` 71 72 URL string `json:"url"` 73 Headers map[string]string `json:"headers"` 74 75 EnvironmentID string `json:"environment_id"` 76 RegionURL string `json:"region_url"` 77 RegionalCredVerificationURL string `json:"regional_cred_verification_url"` 78 CredVerificationURL string `json:"cred_verification_url"` 79 Format format `json:"format"` 80} 81 82// parse determines the type of CredentialSource needed 83func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) { 84 if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" { 85 if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil { 86 if awsVersion != 1 { 87 return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion) 88 } 89 return awsCredentialSource{ 90 EnvironmentID: c.CredentialSource.EnvironmentID, 91 RegionURL: c.CredentialSource.RegionURL, 92 RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL, 93 CredVerificationURL: c.CredentialSource.URL, 94 TargetResource: c.Audience, 95 ctx: ctx, 96 }, nil 97 } 98 } else if c.CredentialSource.File != "" { 99 return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil 100 } else if c.CredentialSource.URL != "" { 101 return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil 102 } 103 return nil, fmt.Errorf("oauth2/google: unable to parse credential source") 104} 105 106type baseCredentialSource interface { 107 subjectToken() (string, error) 108} 109 110// tokenSource is the source that handles external credentials. 111type tokenSource struct { 112 ctx context.Context 113 conf *Config 114} 115 116// Token allows tokenSource to conform to the oauth2.TokenSource interface. 117func (ts tokenSource) Token() (*oauth2.Token, error) { 118 conf := ts.conf 119 120 credSource, err := conf.parse(ts.ctx) 121 if err != nil { 122 return nil, err 123 } 124 subjectToken, err := credSource.subjectToken() 125 126 if err != nil { 127 return nil, err 128 } 129 stsRequest := stsTokenExchangeRequest{ 130 GrantType: "urn:ietf:params:oauth:grant-type:token-exchange", 131 Audience: conf.Audience, 132 Scope: conf.Scopes, 133 RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token", 134 SubjectToken: subjectToken, 135 SubjectTokenType: conf.SubjectTokenType, 136 } 137 header := make(http.Header) 138 header.Add("Content-Type", "application/x-www-form-urlencoded") 139 clientAuth := clientAuthentication{ 140 AuthStyle: oauth2.AuthStyleInHeader, 141 ClientID: conf.ClientID, 142 ClientSecret: conf.ClientSecret, 143 } 144 stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil) 145 if err != nil { 146 return nil, err 147 } 148 149 accessToken := &oauth2.Token{ 150 AccessToken: stsResp.AccessToken, 151 TokenType: stsResp.TokenType, 152 } 153 if stsResp.ExpiresIn < 0 { 154 return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service") 155 } else if stsResp.ExpiresIn >= 0 { 156 accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second) 157 } 158 159 if stsResp.RefreshToken != "" { 160 accessToken.RefreshToken = stsResp.RefreshToken 161 } 162 return accessToken, nil 163} 164