// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package credentials import ( "context" "encoding/json" "errors" "fmt" "net/http" "os" "time" "cloud.google.com/go/auth" "cloud.google.com/go/auth/internal" "cloud.google.com/go/auth/internal/credsfile" "cloud.google.com/go/compute/metadata" ) const ( // jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow. jwtTokenURL = "https://oauth2.googleapis.com/token" // Google's OAuth 2.0 default endpoints. googleAuthURL = "https://accounts.google.com/o/oauth2/auth" googleTokenURL = "https://oauth2.googleapis.com/token" // Help on default credentials adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc" ) var ( // for testing allowOnGCECheck = true ) // OnGCE reports whether this process is running in Google Cloud. func OnGCE() bool { // TODO(codyoss): once all libs use this auth lib move metadata check here return allowOnGCECheck && metadata.OnGCE() } // DetectDefault searches for "Application Default Credentials" and returns // a credential based on the [DetectOptions] provided. // // It looks for credentials in the following places, preferring the first // location found: // // - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS // environment variable. For workload identity federation, refer to // https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation // on how to generate the JSON configuration file for on-prem/non-Google // cloud platforms. // - A JSON file in a location known to the gcloud command-line tool. On // Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On // other systems, $HOME/.config/gcloud/application_default_credentials.json. // - On Google Compute Engine, Google App Engine standard second generation // runtimes, and Google App Engine flexible environment, it fetches // credentials from the metadata server. func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) { if err := opts.validate(); err != nil { return nil, err } if opts.CredentialsJSON != nil { return readCredentialsFileJSON(opts.CredentialsJSON, opts) } if filename := credsfile.GetFileNameFromEnv(opts.CredentialsFile); filename != "" { if creds, err := readCredentialsFile(filename, opts); err == nil { return creds, err } } fileName := credsfile.GetWellKnownFileName() if b, err := os.ReadFile(fileName); err == nil { return readCredentialsFileJSON(b, opts) } if OnGCE() { return auth.NewCredentials(&auth.CredentialsOptions{ TokenProvider: computeTokenProvider(opts.EarlyTokenRefresh, opts.Scopes...), ProjectIDProvider: auth.CredentialsPropertyFunc(func(context.Context) (string, error) { return metadata.ProjectID() }), UniverseDomainProvider: &internal.ComputeUniverseDomainProvider{}, }), nil } return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL) } // DetectOptions provides configuration for [DetectDefault]. type DetectOptions struct { // Scopes that credentials tokens should have. Example: // https://www.googleapis.com/auth/cloud-platform. Required if Audience is // not provided. Scopes []string // Audience that credentials tokens should have. Only applicable for 2LO // flows with service accounts. If specified, scopes should not be provided. Audience string // Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority). // Optional. Subject string // EarlyTokenRefresh configures how early before a token expires that it // should be refreshed. EarlyTokenRefresh time.Duration // AuthHandlerOptions configures an authorization handler and other options // for 3LO flows. It is required, and only used, for client credential // flows. AuthHandlerOptions *auth.AuthorizationHandlerOptions // TokenURL allows to set the token endpoint for user credential flows. If // unset the default value is: https://oauth2.googleapis.com/token. // Optional. TokenURL string // STSAudience is the audience sent to when retrieving an STS token. // Currently this only used for GDCH auth flow, for which it is required. STSAudience string // CredentialsFile overrides detection logic and sources a credential file // from the provided filepath. If provided, CredentialsJSON must not be. // Optional. CredentialsFile string // CredentialsJSON overrides detection logic and uses the JSON bytes as the // source for the credential. If provided, CredentialsFile must not be. // Optional. CredentialsJSON []byte // UseSelfSignedJWT directs service account based credentials to create a // self-signed JWT with the private key found in the file, skipping any // network requests that would normally be made. Optional. UseSelfSignedJWT bool // Client configures the underlying client used to make network requests // when fetching tokens. Optional. Client *http.Client // UniverseDomain is the default service domain for a given Cloud universe. // The default value is "googleapis.com". This option is ignored for // authentication flows that do not support universe domain. Optional. UniverseDomain string } func (o *DetectOptions) validate() error { if o == nil { return errors.New("credentials: options must be provided") } if len(o.Scopes) > 0 && o.Audience != "" { return errors.New("credentials: both scopes and audience were provided") } if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" { return errors.New("credentials: both credentials file and JSON were provided") } return nil } func (o *DetectOptions) tokenURL() string { if o.TokenURL != "" { return o.TokenURL } return googleTokenURL } func (o *DetectOptions) scopes() []string { scopes := make([]string, len(o.Scopes)) copy(scopes, o.Scopes) return scopes } func (o *DetectOptions) client() *http.Client { if o.Client != nil { return o.Client } return internal.CloneDefaultClient() } func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) { b, err := os.ReadFile(filename) if err != nil { return nil, err } return readCredentialsFileJSON(b, opts) } func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) { // attempt to parse jsonData as a Google Developers Console client_credentials.json. config := clientCredConfigFromJSON(b, opts) if config != nil { if config.AuthHandlerOpts == nil { return nil, errors.New("credentials: auth handler must be specified for this credential filetype") } tp, err := auth.New3LOTokenProvider(config) if err != nil { return nil, err } return auth.NewCredentials(&auth.CredentialsOptions{ TokenProvider: tp, JSON: b, }), nil } return fileCredentials(b, opts) } func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO { var creds credsfile.ClientCredentialsFile var c *credsfile.Config3LO if err := json.Unmarshal(b, &creds); err != nil { return nil } switch { case creds.Web != nil: c = creds.Web case creds.Installed != nil: c = creds.Installed default: return nil } if len(c.RedirectURIs) < 1 { return nil } var handleOpts *auth.AuthorizationHandlerOptions if opts.AuthHandlerOptions != nil { handleOpts = &auth.AuthorizationHandlerOptions{ Handler: opts.AuthHandlerOptions.Handler, State: opts.AuthHandlerOptions.State, PKCEOpts: opts.AuthHandlerOptions.PKCEOpts, } } return &auth.Options3LO{ ClientID: c.ClientID, ClientSecret: c.ClientSecret, RedirectURL: c.RedirectURIs[0], Scopes: opts.scopes(), AuthURL: c.AuthURI, TokenURL: c.TokenURI, Client: opts.client(), EarlyTokenExpiry: opts.EarlyTokenRefresh, AuthHandlerOpts: handleOpts, // TODO(codyoss): refactor this out. We need to add in auto-detection // for this use case. AuthStyle: auth.StyleInParams, } }