// 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" "log/slog" "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/auth/internal/trustboundary" "cloud.google.com/go/compute/metadata" "github.com/googleapis/gax-go/v2/internallog" ) 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" // GoogleMTLSTokenURL is Google's default OAuth2.0 mTLS endpoint. GoogleMTLSTokenURL = "https://oauth2.mtls.googleapis.com/token" // Help on default credentials adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc" ) var ( // for testing allowOnGCECheck = true ) // CredType specifies the type of JSON credentials being provided // to a loading function such as [NewCredentialsFromFile] or // [NewCredentialsFromJSON]. type CredType string const ( // ServiceAccount represents a service account file type. ServiceAccount CredType = "service_account" // AuthorizedUser represents a user credentials file type. AuthorizedUser CredType = "authorized_user" // ExternalAccount represents an external account file type. // // IMPORTANT: // This credential type does not validate the credential configuration. A security // risk occurs when a credential configuration configured with malicious urls // is used. // You should validate credential configurations provided by untrusted sources. // See [Security requirements when using credential configurations from an external // source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials // for more details. ExternalAccount CredType = "external_account" // ImpersonatedServiceAccount represents an impersonated service account file type. // // IMPORTANT: // This credential type does not validate the credential configuration. A security // risk occurs when a credential configuration configured with malicious urls // is used. // You should validate credential configurations provided by untrusted sources. // See [Security requirements when using credential configurations from an external // source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials // for more details. ImpersonatedServiceAccount CredType = "impersonated_service_account" // GDCHServiceAccount represents a GDCH service account credentials. GDCHServiceAccount CredType = "gdch_service_account" // ExternalAccountAuthorizedUser represents an external account authorized user credentials. ExternalAccountAuthorizedUser CredType = "external_account_authorized_user" ) // TokenBindingType specifies the type of binding used when requesting a token // whether to request a hard-bound token using mTLS or an instance identity // bound token using ALTS. type TokenBindingType int const ( // NoBinding specifies that requested tokens are not required to have a // binding. This is the default option. NoBinding TokenBindingType = iota // MTLSHardBinding specifies that a hard-bound token should be requested // using an mTLS with S2A channel. MTLSHardBinding // ALTSHardBinding specifies that an instance identity bound token should // be requested using an ALTS channel. ALTSHardBinding ) // 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. // // Important: If you accept a credential configuration (credential // JSON/File/Stream) from an external source for authentication to Google // Cloud Platform, you must validate it before providing it to any Google // API or library. Providing an unvalidated credential configuration to // Google APIs can compromise the security of your systems and data. For // more information, refer to [Validate credential configurations from // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) { if err := opts.validate(); err != nil { return nil, err } trustBoundaryEnabled, err := trustboundary.IsEnabled() if err != nil { return nil, err } if len(opts.CredentialsJSON) > 0 { return readCredentialsFileJSON(opts.CredentialsJSON, opts) } if opts.CredentialsFile != "" { return readCredentialsFile(opts.CredentialsFile, opts) } if filename := os.Getenv(credsfile.GoogleAppCredsEnvVar); filename != "" { creds, err := readCredentialsFile(filename, opts) if err != nil { return nil, err } return creds, nil } fileName := credsfile.GetWellKnownFileName() if b, err := os.ReadFile(fileName); err == nil { return readCredentialsFileJSON(b, opts) } if OnGCE() { metadataClient := metadata.NewWithOptions(&metadata.Options{ Logger: opts.logger(), UseDefaultClient: true, }) gceUniverseDomainProvider := &internal.ComputeUniverseDomainProvider{ MetadataClient: metadataClient, } tp := computeTokenProvider(opts, metadataClient) if trustBoundaryEnabled { gceConfigProvider := trustboundary.NewGCEConfigProvider(gceUniverseDomainProvider) var err error tp, err = trustboundary.NewProvider(opts.client(), gceConfigProvider, opts.logger(), tp) if err != nil { return nil, fmt.Errorf("credentials: failed to initialize GCE trust boundary provider: %w", err) } } return auth.NewCredentials(&auth.CredentialsOptions{ TokenProvider: tp, ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) { return metadataClient.ProjectIDWithContext(ctx) }), UniverseDomainProvider: gceUniverseDomainProvider, }), 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 // TokenBindingType specifies the type of binding used when requesting a // token whether to request a hard-bound token using mTLS or an instance // identity bound token using ALTS. Optional. TokenBindingType TokenBindingType // 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. Once the token’s time until expiration has entered // this refresh window the token is considered valid but stale. If unset, // the default value is 3 minutes and 45 seconds. Optional. EarlyTokenRefresh time.Duration // DisableAsyncRefresh configures a synchronous workflow that refreshes // stale tokens while blocking. The default is false. Optional. DisableAsyncRefresh bool // 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. // // Deprecated: This field is deprecated because of a potential security risk. // It does not validate the credential configuration. The security risk occurs // when a credential configuration is accepted from a source that is not // under your control and used without validation on your side. // // If you know that you will be loading credential configurations of a // specific type, it is recommended to use a credential-type-specific // NewCredentialsFromFile method. This will ensure that an unexpected // credential type with potential for malicious intent is not loaded // unintentionally. You might still have to do validation for certain // credential types. Please follow the recommendation for that method. For // example, if you want to load only service accounts, you can use // // creds, err := credentials.NewCredentialsFromFile(ctx, credentials.ServiceAccount, filename, opts) // // If you are loading your credential configuration from an untrusted source // and have not mitigated the risks (e.g. by validating the configuration // yourself), make these changes as soon as possible to prevent security // risks to your environment. // // Regardless of the method used, it is always your responsibility to // validate configurations received from external sources. // // For more details see: // https://cloud.google.com/docs/authentication/external/externally-sourced-credentials CredentialsFile string // CredentialsJSON overrides detection logic and uses the JSON bytes as the // source for the credential. If provided, CredentialsFile must not be. // Optional. // // Deprecated: This field is deprecated because of a potential security risk. // It does not validate the credential configuration. The security risk occurs // when a credential configuration is accepted from a source that is not // under your control and used without validation on your side. // // If you know that you will be loading credential configurations of a // specific type, it is recommended to use a credential-type-specific // NewCredentialsFromJSON method. This will ensure that an unexpected // credential type with potential for malicious intent is not loaded // unintentionally. You might still have to do validation for certain // credential types. Please follow the recommendation for that method. For // example, if you want to load only service accounts, you can use // // creds, err := credentials.NewCredentialsFromJSON(ctx, credentials.ServiceAccount, json, opts) // // If you are loading your credential configuration from an untrusted source // and have not mitigated the risks (e.g. by validating the configuration // yourself), make these changes as soon as possible to prevent security // risks to your environment. // // Regardless of the method used, it is always your responsibility to // validate configurations received from external sources. // // For more details see: // https://cloud.google.com/docs/authentication/external/externally-sourced-credentials 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 // Logger is used for debug logging. If provided, logging will be enabled // at the loggers configured level. By default logging is disabled unless // enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default // logger will be used. Optional. Logger *slog.Logger } // NewCredentialsFromFile creates a [cloud.google.com/go/auth.Credentials] from // the provided file. The credType argument specifies the expected credential // type. If the file content does not match the expected type, an error is // returned. // // Important: If you accept a credential configuration (credential // JSON/File/Stream) from an external source for authentication to Google // Cloud Platform, you must validate it before providing it to any Google // API or library. Providing an unvalidated credential configuration to // Google APIs can compromise the security of your systems and data. For // more information, refer to [Validate credential configurations from // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). func NewCredentialsFromFile(credType CredType, filename string, opts *DetectOptions) (*auth.Credentials, error) { b, err := os.ReadFile(filename) if err != nil { return nil, err } return NewCredentialsFromJSON(credType, b, opts) } // NewCredentialsFromJSON creates a [cloud.google.com/go/auth.Credentials] from // the provided JSON bytes. The credType argument specifies the expected // credential type. If the JSON does not match the expected type, an error is // returned. // // Important: If you accept a credential configuration (credential // JSON/File/Stream) from an external source for authentication to Google // Cloud Platform, you must validate it before providing it to any Google // API or library. Providing an unvalidated credential configuration to // Google APIs can compromise the security of your systems and data. For // more information, refer to [Validate credential configurations from // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials). func NewCredentialsFromJSON(credType CredType, b []byte, opts *DetectOptions) (*auth.Credentials, error) { if err := checkCredentialType(b, credType); err != nil { return nil, err } // We can't use readCredentialsFileJSON because it does auto-detection // for client_credentials.json which we don't support here (no type field). // Instead, we call fileCredentials just as readCredentialsFileJSON does // when it doesn't detect client_credentials.json. return fileCredentials(b, opts) } func checkCredentialType(b []byte, expected CredType) error { fileType, err := credsfile.ParseFileType(b) if err != nil { return err } if CredType(fileType) != expected { return fmt.Errorf("credentials: expected type %q, found %q", expected, fileType) } return nil } 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.DefaultClient() } func (o *DetectOptions) logger() *slog.Logger { return internallog.New(o.Logger) } 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(), Logger: opts.logger(), EarlyTokenExpiry: opts.EarlyTokenRefresh, AuthHandlerOpts: handleOpts, // TODO(codyoss): refactor this out. We need to add in auto-detection // for this use case. AuthStyle: auth.StyleInParams, } }