| // 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, | |
| } | |
| } |