| // 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/regionalaccessboundary" |
| "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). |
| // |
| // Note: If the detected credential configuration file contains a |
| // `service_account_impersonation_url` field, the returned credentials will |
| // yield tokens that are already impersonated to that target service account. |
| func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) { |
| if err := opts.validate(); err != nil { |
| return nil, err |
| } |
| regionalAccessBoundaryEnabled, err := regionalaccessboundary.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 regionalAccessBoundaryEnabled { |
| gceConfigProvider := regionalaccessboundary.NewGCEConfigProvider(gceUniverseDomainProvider) |
| var err error |
| tp, err = regionalaccessboundary.NewProvider(opts.client(), gceConfigProvider, opts.logger(), tp) |
| if err != nil { |
| return nil, fmt.Errorf("credentials: failed to initialize GCE Regional Access 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, |
| } |
| } |