detect.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package credentials
  15. import (
  16. "context"
  17. "encoding/json"
  18. "errors"
  19. "fmt"
  20. "net/http"
  21. "os"
  22. "time"
  23. "cloud.google.com/go/auth"
  24. "cloud.google.com/go/auth/internal"
  25. "cloud.google.com/go/auth/internal/credsfile"
  26. "cloud.google.com/go/compute/metadata"
  27. )
  28. const (
  29. // jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow.
  30. jwtTokenURL = "https://oauth2.googleapis.com/token"
  31. // Google's OAuth 2.0 default endpoints.
  32. googleAuthURL = "https://accounts.google.com/o/oauth2/auth"
  33. googleTokenURL = "https://oauth2.googleapis.com/token"
  34. // GoogleMTLSTokenURL is Google's default OAuth2.0 mTLS endpoint.
  35. GoogleMTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
  36. // Help on default credentials
  37. adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
  38. )
  39. var (
  40. // for testing
  41. allowOnGCECheck = true
  42. )
  43. // OnGCE reports whether this process is running in Google Cloud.
  44. func OnGCE() bool {
  45. // TODO(codyoss): once all libs use this auth lib move metadata check here
  46. return allowOnGCECheck && metadata.OnGCE()
  47. }
  48. // DetectDefault searches for "Application Default Credentials" and returns
  49. // a credential based on the [DetectOptions] provided.
  50. //
  51. // It looks for credentials in the following places, preferring the first
  52. // location found:
  53. //
  54. // - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS
  55. // environment variable. For workload identity federation, refer to
  56. // https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation
  57. // on how to generate the JSON configuration file for on-prem/non-Google
  58. // cloud platforms.
  59. // - A JSON file in a location known to the gcloud command-line tool. On
  60. // Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On
  61. // other systems, $HOME/.config/gcloud/application_default_credentials.json.
  62. // - On Google Compute Engine, Google App Engine standard second generation
  63. // runtimes, and Google App Engine flexible environment, it fetches
  64. // credentials from the metadata server.
  65. func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
  66. if err := opts.validate(); err != nil {
  67. return nil, err
  68. }
  69. if len(opts.CredentialsJSON) > 0 {
  70. return readCredentialsFileJSON(opts.CredentialsJSON, opts)
  71. }
  72. if opts.CredentialsFile != "" {
  73. return readCredentialsFile(opts.CredentialsFile, opts)
  74. }
  75. if filename := os.Getenv(credsfile.GoogleAppCredsEnvVar); filename != "" {
  76. creds, err := readCredentialsFile(filename, opts)
  77. if err != nil {
  78. return nil, err
  79. }
  80. return creds, nil
  81. }
  82. fileName := credsfile.GetWellKnownFileName()
  83. if b, err := os.ReadFile(fileName); err == nil {
  84. return readCredentialsFileJSON(b, opts)
  85. }
  86. if OnGCE() {
  87. return auth.NewCredentials(&auth.CredentialsOptions{
  88. TokenProvider: computeTokenProvider(opts),
  89. ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
  90. return metadata.ProjectIDWithContext(ctx)
  91. }),
  92. UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{},
  93. }), nil
  94. }
  95. return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL)
  96. }
  97. // DetectOptions provides configuration for [DetectDefault].
  98. type DetectOptions struct {
  99. // Scopes that credentials tokens should have. Example:
  100. // https://www.googleapis.com/auth/cloud-platform. Required if Audience is
  101. // not provided.
  102. Scopes []string
  103. // Audience that credentials tokens should have. Only applicable for 2LO
  104. // flows with service accounts. If specified, scopes should not be provided.
  105. Audience string
  106. // Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
  107. // Optional.
  108. Subject string
  109. // EarlyTokenRefresh configures how early before a token expires that it
  110. // should be refreshed. Once the token’s time until expiration has entered
  111. // this refresh window the token is considered valid but stale. If unset,
  112. // the default value is 3 minutes and 45 seconds. Optional.
  113. EarlyTokenRefresh time.Duration
  114. // DisableAsyncRefresh configures a synchronous workflow that refreshes
  115. // stale tokens while blocking. The default is false. Optional.
  116. DisableAsyncRefresh bool
  117. // AuthHandlerOptions configures an authorization handler and other options
  118. // for 3LO flows. It is required, and only used, for client credential
  119. // flows.
  120. AuthHandlerOptions *auth.AuthorizationHandlerOptions
  121. // TokenURL allows to set the token endpoint for user credential flows. If
  122. // unset the default value is: https://oauth2.googleapis.com/token.
  123. // Optional.
  124. TokenURL string
  125. // STSAudience is the audience sent to when retrieving an STS token.
  126. // Currently this only used for GDCH auth flow, for which it is required.
  127. STSAudience string
  128. // CredentialsFile overrides detection logic and sources a credential file
  129. // from the provided filepath. If provided, CredentialsJSON must not be.
  130. // Optional.
  131. CredentialsFile string
  132. // CredentialsJSON overrides detection logic and uses the JSON bytes as the
  133. // source for the credential. If provided, CredentialsFile must not be.
  134. // Optional.
  135. CredentialsJSON []byte
  136. // UseSelfSignedJWT directs service account based credentials to create a
  137. // self-signed JWT with the private key found in the file, skipping any
  138. // network requests that would normally be made. Optional.
  139. UseSelfSignedJWT bool
  140. // Client configures the underlying client used to make network requests
  141. // when fetching tokens. Optional.
  142. Client *http.Client
  143. // UniverseDomain is the default service domain for a given Cloud universe.
  144. // The default value is "googleapis.com". This option is ignored for
  145. // authentication flows that do not support universe domain. Optional.
  146. UniverseDomain string
  147. }
  148. func (o *DetectOptions) validate() error {
  149. if o == nil {
  150. return errors.New("credentials: options must be provided")
  151. }
  152. if len(o.Scopes) > 0 && o.Audience != "" {
  153. return errors.New("credentials: both scopes and audience were provided")
  154. }
  155. if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" {
  156. return errors.New("credentials: both credentials file and JSON were provided")
  157. }
  158. return nil
  159. }
  160. func (o *DetectOptions) tokenURL() string {
  161. if o.TokenURL != "" {
  162. return o.TokenURL
  163. }
  164. return googleTokenURL
  165. }
  166. func (o *DetectOptions) scopes() []string {
  167. scopes := make([]string, len(o.Scopes))
  168. copy(scopes, o.Scopes)
  169. return scopes
  170. }
  171. func (o *DetectOptions) client() *http.Client {
  172. if o.Client != nil {
  173. return o.Client
  174. }
  175. return internal.DefaultClient()
  176. }
  177. func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) {
  178. b, err := os.ReadFile(filename)
  179. if err != nil {
  180. return nil, err
  181. }
  182. return readCredentialsFileJSON(b, opts)
  183. }
  184. func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
  185. // attempt to parse jsonData as a Google Developers Console client_credentials.json.
  186. config := clientCredConfigFromJSON(b, opts)
  187. if config != nil {
  188. if config.AuthHandlerOpts == nil {
  189. return nil, errors.New("credentials: auth handler must be specified for this credential filetype")
  190. }
  191. tp, err := auth.New3LOTokenProvider(config)
  192. if err != nil {
  193. return nil, err
  194. }
  195. return auth.NewCredentials(&auth.CredentialsOptions{
  196. TokenProvider: tp,
  197. JSON: b,
  198. }), nil
  199. }
  200. return fileCredentials(b, opts)
  201. }
  202. func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
  203. var creds credsfile.ClientCredentialsFile
  204. var c *credsfile.Config3LO
  205. if err := json.Unmarshal(b, &creds); err != nil {
  206. return nil
  207. }
  208. switch {
  209. case creds.Web != nil:
  210. c = creds.Web
  211. case creds.Installed != nil:
  212. c = creds.Installed
  213. default:
  214. return nil
  215. }
  216. if len(c.RedirectURIs) < 1 {
  217. return nil
  218. }
  219. var handleOpts *auth.AuthorizationHandlerOptions
  220. if opts.AuthHandlerOptions != nil {
  221. handleOpts = &auth.AuthorizationHandlerOptions{
  222. Handler: opts.AuthHandlerOptions.Handler,
  223. State: opts.AuthHandlerOptions.State,
  224. PKCEOpts: opts.AuthHandlerOptions.PKCEOpts,
  225. }
  226. }
  227. return &auth.Options3LO{
  228. ClientID: c.ClientID,
  229. ClientSecret: c.ClientSecret,
  230. RedirectURL: c.RedirectURIs[0],
  231. Scopes: opts.scopes(),
  232. AuthURL: c.AuthURI,
  233. TokenURL: c.TokenURI,
  234. Client: opts.client(),
  235. EarlyTokenExpiry: opts.EarlyTokenRefresh,
  236. AuthHandlerOpts: handleOpts,
  237. // TODO(codyoss): refactor this out. We need to add in auto-detection
  238. // for this use case.
  239. AuthStyle: auth.StyleInParams,
  240. }
  241. }