basecredentials.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. // ClientSecret is currently only required if token_info endpoint also
  38. // needs to be called with the generated GCP access token. When provided, STS will be
  39. // called with additional basic authentication using client_id as username and client_secret as password.
  40. ClientSecret string
  41. // ClientID is only required in conjunction with ClientSecret, as described above.
  42. ClientID string
  43. // CredentialSource contains the necessary information to retrieve the token itself, as well
  44. // as some environmental information.
  45. CredentialSource CredentialSource
  46. // QuotaProjectID is injected by gCloud. If the value is non-empty, the Auth libraries
  47. // will set the x-goog-user-project which overrides the project associated with the credentials.
  48. QuotaProjectID string
  49. // Scopes contains the desired scopes for the returned access token.
  50. Scopes []string
  51. }
  52. // Each element consists of a list of patterns. validateURLs checks for matches
  53. // that include all elements in a given list, in that order.
  54. var (
  55. validTokenURLPatterns = []*regexp.Regexp{
  56. // The complicated part in the middle matches any number of characters that
  57. // aren't period, spaces, or slashes.
  58. regexp.MustCompile(`(?i)^[^\.\s\/\\]+\.sts\.googleapis\.com$`),
  59. regexp.MustCompile(`(?i)^sts\.googleapis\.com$`),
  60. regexp.MustCompile(`(?i)^sts\.[^\.\s\/\\]+\.googleapis\.com$`),
  61. regexp.MustCompile(`(?i)^[^\.\s\/\\]+-sts\.googleapis\.com$`),
  62. }
  63. validImpersonateURLPatterns = []*regexp.Regexp{
  64. regexp.MustCompile(`^[^\.\s\/\\]+\.iamcredentials\.googleapis\.com$`),
  65. regexp.MustCompile(`^iamcredentials\.googleapis\.com$`),
  66. regexp.MustCompile(`^iamcredentials\.[^\.\s\/\\]+\.googleapis\.com$`),
  67. regexp.MustCompile(`^[^\.\s\/\\]+-iamcredentials\.googleapis\.com$`),
  68. }
  69. )
  70. func validateURL(input string, patterns []*regexp.Regexp, scheme string) bool {
  71. parsed, err := url.Parse(input)
  72. if err != nil {
  73. return false
  74. }
  75. if !strings.EqualFold(parsed.Scheme, scheme) {
  76. return false
  77. }
  78. toTest := parsed.Host
  79. for _, pattern := range patterns {
  80. if valid := pattern.MatchString(toTest); valid {
  81. return true
  82. }
  83. }
  84. return false
  85. }
  86. // TokenSource Returns an external account TokenSource struct. This is to be called by package google to construct a google.Credentials.
  87. func (c *Config) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
  88. return c.tokenSource(ctx, validTokenURLPatterns, validImpersonateURLPatterns, "https")
  89. }
  90. // tokenSource is a private function that's directly called by some of the tests,
  91. // because the unit test URLs are mocked, and would otherwise fail the
  92. // validity check.
  93. func (c *Config) tokenSource(ctx context.Context, tokenURLValidPats []*regexp.Regexp, impersonateURLValidPats []*regexp.Regexp, scheme string) (oauth2.TokenSource, error) {
  94. valid := validateURL(c.TokenURL, tokenURLValidPats, scheme)
  95. if !valid {
  96. return nil, fmt.Errorf("oauth2/google: invalid TokenURL provided while constructing tokenSource")
  97. }
  98. if c.ServiceAccountImpersonationURL != "" {
  99. valid := validateURL(c.ServiceAccountImpersonationURL, impersonateURLValidPats, scheme)
  100. if !valid {
  101. return nil, fmt.Errorf("oauth2/google: invalid ServiceAccountImpersonationURL provided while constructing tokenSource")
  102. }
  103. }
  104. ts := tokenSource{
  105. ctx: ctx,
  106. conf: c,
  107. }
  108. if c.ServiceAccountImpersonationURL == "" {
  109. return oauth2.ReuseTokenSource(nil, ts), nil
  110. }
  111. scopes := c.Scopes
  112. ts.conf.Scopes = []string{"https://www.googleapis.com/auth/cloud-platform"}
  113. imp := impersonateTokenSource{
  114. ctx: ctx,
  115. url: c.ServiceAccountImpersonationURL,
  116. scopes: scopes,
  117. ts: oauth2.ReuseTokenSource(nil, ts),
  118. }
  119. return oauth2.ReuseTokenSource(nil, imp), nil
  120. }
  121. // Subject token file types.
  122. const (
  123. fileTypeText = "text"
  124. fileTypeJSON = "json"
  125. )
  126. type format struct {
  127. // Type is either "text" or "json". When not provided "text" type is assumed.
  128. Type string `json:"type"`
  129. // SubjectTokenFieldName is only required for JSON format. This would be "access_token" for azure.
  130. SubjectTokenFieldName string `json:"subject_token_field_name"`
  131. }
  132. // CredentialSource stores the information necessary to retrieve the credentials for the STS exchange.
  133. // Either the File or the URL field should be filled, depending on the kind of credential in question.
  134. // The EnvironmentID should start with AWS if being used for an AWS credential.
  135. type CredentialSource struct {
  136. File string `json:"file"`
  137. URL string `json:"url"`
  138. Headers map[string]string `json:"headers"`
  139. EnvironmentID string `json:"environment_id"`
  140. RegionURL string `json:"region_url"`
  141. RegionalCredVerificationURL string `json:"regional_cred_verification_url"`
  142. CredVerificationURL string `json:"cred_verification_url"`
  143. Format format `json:"format"`
  144. }
  145. // parse determines the type of CredentialSource needed
  146. func (c *Config) parse(ctx context.Context) (baseCredentialSource, error) {
  147. if len(c.CredentialSource.EnvironmentID) > 3 && c.CredentialSource.EnvironmentID[:3] == "aws" {
  148. if awsVersion, err := strconv.Atoi(c.CredentialSource.EnvironmentID[3:]); err == nil {
  149. if awsVersion != 1 {
  150. return nil, fmt.Errorf("oauth2/google: aws version '%d' is not supported in the current build", awsVersion)
  151. }
  152. return awsCredentialSource{
  153. EnvironmentID: c.CredentialSource.EnvironmentID,
  154. RegionURL: c.CredentialSource.RegionURL,
  155. RegionalCredVerificationURL: c.CredentialSource.RegionalCredVerificationURL,
  156. CredVerificationURL: c.CredentialSource.URL,
  157. TargetResource: c.Audience,
  158. ctx: ctx,
  159. }, nil
  160. }
  161. } else if c.CredentialSource.File != "" {
  162. return fileCredentialSource{File: c.CredentialSource.File, Format: c.CredentialSource.Format}, nil
  163. } else if c.CredentialSource.URL != "" {
  164. return urlCredentialSource{URL: c.CredentialSource.URL, Headers: c.CredentialSource.Headers, Format: c.CredentialSource.Format, ctx: ctx}, nil
  165. }
  166. return nil, fmt.Errorf("oauth2/google: unable to parse credential source")
  167. }
  168. type baseCredentialSource interface {
  169. subjectToken() (string, error)
  170. }
  171. // tokenSource is the source that handles external credentials. It is used to retrieve Tokens.
  172. type tokenSource struct {
  173. ctx context.Context
  174. conf *Config
  175. }
  176. // Token allows tokenSource to conform to the oauth2.TokenSource interface.
  177. func (ts tokenSource) Token() (*oauth2.Token, error) {
  178. conf := ts.conf
  179. credSource, err := conf.parse(ts.ctx)
  180. if err != nil {
  181. return nil, err
  182. }
  183. subjectToken, err := credSource.subjectToken()
  184. if err != nil {
  185. return nil, err
  186. }
  187. stsRequest := stsTokenExchangeRequest{
  188. GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
  189. Audience: conf.Audience,
  190. Scope: conf.Scopes,
  191. RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
  192. SubjectToken: subjectToken,
  193. SubjectTokenType: conf.SubjectTokenType,
  194. }
  195. header := make(http.Header)
  196. header.Add("Content-Type", "application/x-www-form-urlencoded")
  197. clientAuth := clientAuthentication{
  198. AuthStyle: oauth2.AuthStyleInHeader,
  199. ClientID: conf.ClientID,
  200. ClientSecret: conf.ClientSecret,
  201. }
  202. stsResp, err := exchangeToken(ts.ctx, conf.TokenURL, &stsRequest, clientAuth, header, nil)
  203. if err != nil {
  204. return nil, err
  205. }
  206. accessToken := &oauth2.Token{
  207. AccessToken: stsResp.AccessToken,
  208. TokenType: stsResp.TokenType,
  209. }
  210. if stsResp.ExpiresIn < 0 {
  211. return nil, fmt.Errorf("oauth2/google: got invalid expiry from security token service")
  212. } else if stsResp.ExpiresIn >= 0 {
  213. accessToken.Expiry = now().Add(time.Duration(stsResp.ExpiresIn) * time.Second)
  214. }
  215. if stsResp.RefreshToken != "" {
  216. accessToken.RefreshToken = stsResp.RefreshToken
  217. }
  218. return accessToken, nil
  219. }