creds.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. // Copyright 2017 Google LLC.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package internal
  5. import (
  6. "context"
  7. "crypto/tls"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "net"
  12. "net/http"
  13. "os"
  14. "time"
  15. "golang.org/x/oauth2"
  16. "google.golang.org/api/internal/cert"
  17. "google.golang.org/api/internal/impersonate"
  18. "golang.org/x/oauth2/google"
  19. )
  20. const quotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
  21. // Creds returns credential information obtained from DialSettings, or if none, then
  22. // it returns default credential information.
  23. func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
  24. creds, err := baseCreds(ctx, ds)
  25. if err != nil {
  26. return nil, err
  27. }
  28. if ds.ImpersonationConfig != nil {
  29. return impersonateCredentials(ctx, creds, ds)
  30. }
  31. return creds, nil
  32. }
  33. func baseCreds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
  34. if ds.InternalCredentials != nil {
  35. return ds.InternalCredentials, nil
  36. }
  37. if ds.Credentials != nil {
  38. return ds.Credentials, nil
  39. }
  40. if ds.CredentialsJSON != nil {
  41. return credentialsFromJSON(ctx, ds.CredentialsJSON, ds)
  42. }
  43. if ds.CredentialsFile != "" {
  44. data, err := os.ReadFile(ds.CredentialsFile)
  45. if err != nil {
  46. return nil, fmt.Errorf("cannot read credentials file: %v", err)
  47. }
  48. return credentialsFromJSON(ctx, data, ds)
  49. }
  50. if ds.TokenSource != nil {
  51. return &google.Credentials{TokenSource: ds.TokenSource}, nil
  52. }
  53. cred, err := google.FindDefaultCredentials(ctx, ds.GetScopes()...)
  54. if err != nil {
  55. return nil, err
  56. }
  57. if len(cred.JSON) > 0 {
  58. return credentialsFromJSON(ctx, cred.JSON, ds)
  59. }
  60. // For GAE and GCE, the JSON is empty so return the default credentials directly.
  61. return cred, nil
  62. }
  63. // JSON key file type.
  64. const (
  65. serviceAccountKey = "service_account"
  66. )
  67. // credentialsFromJSON returns a google.Credentials from the JSON data
  68. //
  69. // - A self-signed JWT flow will be executed if the following conditions are
  70. // met:
  71. //
  72. // (1) At least one of the following is true:
  73. // (a) Scope for self-signed JWT flow is enabled
  74. // (b) Audiences are explicitly provided by users
  75. // (2) No service account impersontation
  76. //
  77. // - Otherwise, executes standard OAuth 2.0 flow
  78. // More details: google.aip.dev/auth/4111
  79. func credentialsFromJSON(ctx context.Context, data []byte, ds *DialSettings) (*google.Credentials, error) {
  80. var params google.CredentialsParams
  81. params.Scopes = ds.GetScopes()
  82. // Determine configurations for the OAuth2 transport, which is separate from the API transport.
  83. // The OAuth2 transport and endpoint will be configured for mTLS if applicable.
  84. clientCertSource, err := getClientCertificateSource(ds)
  85. if err != nil {
  86. return nil, err
  87. }
  88. params.TokenURL = oAuth2Endpoint(clientCertSource)
  89. if clientCertSource != nil {
  90. tlsConfig := &tls.Config{
  91. GetClientCertificate: clientCertSource,
  92. }
  93. ctx = context.WithValue(ctx, oauth2.HTTPClient, customHTTPClient(tlsConfig))
  94. }
  95. // By default, a standard OAuth 2.0 token source is created
  96. cred, err := google.CredentialsFromJSONWithParams(ctx, data, params)
  97. if err != nil {
  98. return nil, err
  99. }
  100. // Override the token source to use self-signed JWT if conditions are met
  101. isJWTFlow, err := isSelfSignedJWTFlow(data, ds)
  102. if err != nil {
  103. return nil, err
  104. }
  105. if isJWTFlow {
  106. ts, err := selfSignedJWTTokenSource(data, ds)
  107. if err != nil {
  108. return nil, err
  109. }
  110. cred.TokenSource = ts
  111. }
  112. return cred, err
  113. }
  114. func oAuth2Endpoint(clientCertSource cert.Source) string {
  115. if isMTLS(clientCertSource) {
  116. return google.MTLSTokenURL
  117. }
  118. return google.Endpoint.TokenURL
  119. }
  120. func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) {
  121. // For non-GDU universe domains, token exchange is impossible and services
  122. // must support self-signed JWTs with scopes.
  123. if !ds.IsUniverseDomainGDU() {
  124. return typeServiceAccount(data)
  125. }
  126. if (ds.EnableJwtWithScope || ds.HasCustomAudience()) && ds.ImpersonationConfig == nil {
  127. return typeServiceAccount(data)
  128. }
  129. return false, nil
  130. }
  131. // typeServiceAccount checks if JSON data is for a service account.
  132. func typeServiceAccount(data []byte) (bool, error) {
  133. var f struct {
  134. Type string `json:"type"`
  135. // The remaining JSON fields are omitted because they are not used.
  136. }
  137. if err := json.Unmarshal(data, &f); err != nil {
  138. return false, err
  139. }
  140. return f.Type == serviceAccountKey, nil
  141. }
  142. func selfSignedJWTTokenSource(data []byte, ds *DialSettings) (oauth2.TokenSource, error) {
  143. if len(ds.GetScopes()) > 0 && !ds.HasCustomAudience() {
  144. // Scopes are preferred in self-signed JWT unless the scope is not available
  145. // or a custom audience is used.
  146. return google.JWTAccessTokenSourceWithScope(data, ds.GetScopes()...)
  147. } else if ds.GetAudience() != "" {
  148. // Fallback to audience if scope is not provided
  149. return google.JWTAccessTokenSourceFromJSON(data, ds.GetAudience())
  150. } else {
  151. return nil, errors.New("neither scopes or audience are available for the self-signed JWT")
  152. }
  153. }
  154. // GetQuotaProject retrieves quota project with precedence being: client option,
  155. // environment variable, creds file.
  156. func GetQuotaProject(creds *google.Credentials, clientOpt string) string {
  157. if clientOpt != "" {
  158. return clientOpt
  159. }
  160. if env := os.Getenv(quotaProjectEnvVar); env != "" {
  161. return env
  162. }
  163. if creds == nil {
  164. return ""
  165. }
  166. var v struct {
  167. QuotaProject string `json:"quota_project_id"`
  168. }
  169. if err := json.Unmarshal(creds.JSON, &v); err != nil {
  170. return ""
  171. }
  172. return v.QuotaProject
  173. }
  174. func impersonateCredentials(ctx context.Context, creds *google.Credentials, ds *DialSettings) (*google.Credentials, error) {
  175. if len(ds.ImpersonationConfig.Scopes) == 0 {
  176. ds.ImpersonationConfig.Scopes = ds.GetScopes()
  177. }
  178. ts, err := impersonate.TokenSource(ctx, creds.TokenSource, ds.ImpersonationConfig)
  179. if err != nil {
  180. return nil, err
  181. }
  182. return &google.Credentials{
  183. TokenSource: ts,
  184. ProjectID: creds.ProjectID,
  185. }, nil
  186. }
  187. // customHTTPClient constructs an HTTPClient using the provided tlsConfig, to support mTLS.
  188. func customHTTPClient(tlsConfig *tls.Config) *http.Client {
  189. trans := baseTransport()
  190. trans.TLSClientConfig = tlsConfig
  191. return &http.Client{Transport: trans}
  192. }
  193. func baseTransport() *http.Transport {
  194. return &http.Transport{
  195. Proxy: http.ProxyFromEnvironment,
  196. DialContext: (&net.Dialer{
  197. Timeout: 30 * time.Second,
  198. KeepAlive: 30 * time.Second,
  199. DualStack: true,
  200. }).DialContext,
  201. MaxIdleConns: 100,
  202. MaxIdleConnsPerHost: 100,
  203. IdleConnTimeout: 90 * time.Second,
  204. TLSHandshakeTimeout: 10 * time.Second,
  205. ExpectContinueTimeout: 1 * time.Second,
  206. }
  207. }
  208. // ErrUniverseNotMatch composes an error string from the provided universe
  209. // domain sources (DialSettings and Credentials, respectively).
  210. func ErrUniverseNotMatch(settingsUD, credsUD string) error {
  211. return fmt.Errorf(
  212. "the configured universe domain (%q) does not match the universe "+
  213. "domain found in the credentials (%q). If you haven't configured "+
  214. "WithUniverseDomain explicitly, \"googleapis.com\" is the default",
  215. settingsUD,
  216. credsUD)
  217. }