basecredentials.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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. package externalaccount
  5. import (
  6. "context"
  7. "fmt"
  8. "net/http"
  9. "regexp"
  10. "strconv"
  11. "time"
  12. "golang.org/x/oauth2"
  13. "golang.org/x/oauth2/google/internal/stsexchange"
  14. )
  15. // now aliases time.Now for testing
  16. var now = func() time.Time {
  17. return time.Now().UTC()
  18. }
  19. // Config stores the configuration for fetching tokens with external credentials.
  20. type Config struct {
  21. // Audience is the Secure Token Service (STS) audience which contains the resource name for the workload
  22. // identity pool or the workforce pool and the provider identifier in that pool.
  23. Audience string
  24. // SubjectTokenType is the STS token type based on the Oauth2.0 token exchange spec
  25. // e.g. `urn:ietf:params:oauth:token-type:jwt`.
  26. SubjectTokenType string
  27. // TokenURL is the STS token exchange endpoint.
  28. TokenURL string
  29. // TokenInfoURL is the token_info endpoint used to retrieve the account related information (
  30. // user attributes like account identifier, eg. email, username, uid, etc). This is
  31. // needed for gCloud session account identification.
  32. TokenInfoURL string
  33. // ServiceAccountImpersonationURL is the URL for the service account impersonation request. This is only
  34. // required for workload identity pools when APIs to be accessed have not integrated with UberMint.
  35. ServiceAccountImpersonationURL string
  36. // ServiceAccountImpersonationLifetimeSeconds is the number of seconds the service account impersonation
  37. // token will be valid for.
  38. ServiceAccountImpersonationLifetimeSeconds int
  39. // ClientSecret is currently only required if token_info endpoint also
  40. // needs to be called with the generated GCP access token. When provided, STS will be
  41. // called with additional basic authentication using client_id as username and client_secret as password.
  42. ClientSecret string
  43. // ClientID is only required in conjunction with ClientSecret, as described above.
  44. ClientID string
  45. // CredentialSource contains the necessary information to retrieve the token itself, as well
  46. // as some environmental information.
  47. CredentialSource CredentialSource
  48. // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
  49. // will set the x-goog-user-project which overrides the project associated with the credentials.
  50. QuotaProjectID string
  51. // Scopes contains the desired scopes for the returned access token.
  52. Scopes []string
  53. // The optional workforce pool user project number when the credential
  54. // corresponds to a workforce pool and not a workload identity pool.
  55. // The underlying principal must still have serviceusage.services.use IAM
  56. // permission to use the project for billing/quota.
  57. WorkforcePoolUserProject string
  58. }
  59. var (
  60. validWorkforceAudiencePattern *regexp.Regexp = regexp.MustCompile(`//iam\.googleapis\.com/locations/[^/]+/workforcePools/`)
  61. )
  62. func validateWorkforceAudience(input string) bool {
  63. return validWorkforceAudiencePattern.MatchString(input)
  64. }
  65. // TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
  66. func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
  67. return c.tokenSource(ctx, "https")
  68. }
  69. // tokenSource is a private function that's directly called by some of the tests,
  70. // because the unit test URLs are mocked, and would otherwise fail the
  71. // validity check.
  72. func (c *Config) tokenSource(ctx context.Context, scheme string) (oauth2.TokenSource, error) {
  73. if c.WorkforcePoolUserProject != "" {
  74. valid := validateWorkforceAudience(c.Audience)
  75. if !valid {
  76. return nil, fmt.Errorf("oauth2/google: workforce_pool_user_project should not be set for non-workforce pool credentials")
  77. }
  78. }
  79. ts := tokenSource{
  80. ctx: ctx,
  81. conf: c,
  82. }
  83. if c.ServiceAccountImpersonationURL == "" {
  84. return oauth2.ReuseTokenSource(nil, ts), nil
  85. }
  86. scopes := c.Scopes
  87. ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
  88. imp := ImpersonateTokenSource{
  89. Ctx: ctx,
  90. URL: c.ServiceAccountImpersonationURL,
  91. Scopes: scopes,
  92. Ts: oauth2.ReuseTokenSource(nil, ts),
  93. TokenLifetimeSeconds: c.ServiceAccountImpersonationLifetimeSeconds,
  94. }
  95. return oauth2.ReuseTokenSource(nil, imp), nil
  96. }
  97. // Subject token file types.
  98. const (
  99. fileTypeText = "text"
  100. fileTypeJSON = "json"
  101. )
  102. type format struct {
  103. // Type is either "text" or "json". When not provided "text" type is assumed.
  104. Type string `json:"type"`
  105. // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
  106. SubjectTokenFieldName string `json:"subject_token_field_name"`
  107. }
  108. // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
  109. // One field amongst File, URL, and Executable should be filled, depending on the kind of credential in question.
  110. // The EnvironmentID should start with AWS if being used for an AWS credential.
  111. type CredentialSource struct {
  112. File string `json:"file"`
  113. URL string `json:"url"`
  114. Headers map[string]string `json:"headers"`
  115. Executable *ExecutableConfig `json:"executable"`
  116. EnvironmentID string `json:"environment_id"`
  117. RegionURL string `json:"region_url"`
  118. RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
  119. CredVerificationURL string `json:"cred_verification_url"`
  120. IMDSv2SessionTokenURL string `json:"imdsv2_session_token_url"`
  121. Format format `json:"format"`
  122. }
  123. type ExecutableConfig struct {
  124. Command string `json:"command"`
  125. TimeoutMillis *int `json:"timeout_millis"`
  126. OutputFile string `json:"output_file"`
  127. }
  128. // parse determines the type of CredentialSource needed.
  129. func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
  130. if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
  131. if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
  132. if awsVersion != 1 {
  133. return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion)
  134. }
  135. awsCredSource := awsCredentialSource{
  136. EnvironmentID: c.CredentialSource.EnvironmentID,
  137. RegionURL: c.CredentialSource.RegionURL,
  138. RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
  139. CredVerificationURL: c.CredentialSource.URL,
  140. TargetResource: c.Audience,
  141. ctx: ctx,
  142. }
  143. if c.CredentialSource.IMDSv2SessionTokenURL != "" {
  144. awsCredSource.IMDSv2SessionTokenURL = c.CredentialSource.IMDSv2SessionTokenURL
  145. }
  146. return awsCredSource, nil
  147. }
  148. } else if c.CredentialSource.File != "" {
  149. return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
  150. } else if c.CredentialSource.URL != "" {
  151. return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
  152. } else if c.CredentialSource.Executable != nil {
  153. return CreateExecutableCredential(ctx, c.CredentialSource.Executable, c)
  154. }
  155. return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
  156. }
  157. type baseCredentialSource interface {
  158. credentialSourceType() string
  159. subjectToken() (string, error)
  160. }
  161. // tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
  162. type tokenSource struct {
  163. ctx context.Context
  164. conf *Config
  165. }
  166. func getMetricsHeaderValue(conf *Config, credSource baseCredentialSource) string {
  167. return fmt.Sprintf("gl-go/%s auth/%s google-byoid-sdk source/%s sa-impersonation/%t config-lifetime/%t",
  168. goVersion(),
  169. "unknown",
  170. credSource.credentialSourceType(),
  171. conf.ServiceAccountImpersonationURL != "",
  172. conf.ServiceAccountImpersonationLifetimeSeconds != 0)
  173. }
  174. // Token allows tokenSource to conform to the oauth2.TokenSource interface.
  175. func (ts tokenSource) Token() (*oauth2.Token, error) {
  176. conf := ts.conf
  177. credSource, err := conf.parse(ts.ctx)
  178. if err != nil {
  179. return nil, err
  180. }
  181. subjectToken, err := credSource.subjectToken()
  182. if err != nil {
  183. return nil, err
  184. }
  185. stsRequest := stsexchange.TokenExchangeRequest{
  186. GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
  187. Audience: conf.Audience,
  188. Scope: conf.Scopes,
  189. RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
  190. SubjectToken: subjectToken,
  191. SubjectTokenType: conf.SubjectTokenType,
  192. }
  193. header := make(http.Header)
  194. header.Add("Content-Type", "application/x-www-form-urlencoded")
  195. header.Add("x-goog-api-client", getMetricsHeaderValue(conf, credSource))
  196. clientAuth := stsexchange.ClientAuthentication{
  197. AuthStyle: oauth2.AuthStyleInHeader,
  198. ClientID: conf.ClientID,
  199. ClientSecret: conf.ClientSecret,
  200. }
  201. var options map[string]interface{}
  202. // Do not pass workforce_pool_user_project when client authentication is used.
  203. // The client ID is sufficient for determining the user project.
  204. if conf.WorkforcePoolUserProject != "" && conf.ClientID == "" {
  205. options = map[string]interface{}{
  206. "userProject": conf.WorkforcePoolUserProject,
  207. }
  208. }
  209. stsResp, err := stsexchange.ExchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, options)
  210. if err != nil {
  211. return nil, err
  212. }
  213. accessToken := &oauth2.Token{
  214. AccessToken: stsResp.AccessToken,
  215. TokenType: stsResp.TokenType,
  216. }
  217. if stsResp.ExpiresIn < 0 {
  218. return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service")
  219. } else if stsResp.ExpiresIn >= 0 {
  220. accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
  221. }
  222. if stsResp.RefreshToken != "" {
  223. accessToken.RefreshToken = stsResp.RefreshToken
  224. }
  225. return accessToken, nil
  226. }