basecredentials.go 9.7 KB

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