detect.go raw

   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  
  15  package credentials
  16  
  17  import (
  18  	"context"
  19  	"encoding/json"
  20  	"errors"
  21  	"fmt"
  22  	"log/slog"
  23  	"net/http"
  24  	"os"
  25  	"time"
  26  
  27  	"cloud.google.com/go/auth"
  28  	"cloud.google.com/go/auth/internal"
  29  	"cloud.google.com/go/auth/internal/credsfile"
  30  	"cloud.google.com/go/auth/internal/trustboundary"
  31  	"cloud.google.com/go/compute/metadata"
  32  	"github.com/googleapis/gax-go/v2/internallog"
  33  )
  34  
  35  const (
  36  	// jwtTokenURL is Google's OAuth 2.0 token URL to use with the JWT(2LO) flow.
  37  	jwtTokenURL = "https://oauth2.googleapis.com/token"
  38  
  39  	// Google's OAuth 2.0 default endpoints.
  40  	googleAuthURL  = "https://accounts.google.com/o/oauth2/auth"
  41  	googleTokenURL = "https://oauth2.googleapis.com/token"
  42  
  43  	// GoogleMTLSTokenURL is Google's default OAuth2.0 mTLS endpoint.
  44  	GoogleMTLSTokenURL = "https://oauth2.mtls.googleapis.com/token"
  45  
  46  	// Help on default credentials
  47  	adcSetupURL = "https://cloud.google.com/docs/authentication/external/set-up-adc"
  48  )
  49  
  50  var (
  51  	// for testing
  52  	allowOnGCECheck = true
  53  )
  54  
  55  // CredType specifies the type of JSON credentials being provided
  56  // to a loading function such as [NewCredentialsFromFile] or
  57  // [NewCredentialsFromJSON].
  58  type CredType string
  59  
  60  const (
  61  	// ServiceAccount represents a service account file type.
  62  	ServiceAccount CredType = "service_account"
  63  	// AuthorizedUser represents a user credentials file type.
  64  	AuthorizedUser CredType = "authorized_user"
  65  	// ExternalAccount represents an external account file type.
  66  	//
  67  	// IMPORTANT:
  68  	// This credential type does not validate the credential configuration. A security
  69  	// risk occurs when a credential configuration configured with malicious urls
  70  	// is used.
  71  	// You should validate credential configurations provided by untrusted sources.
  72  	// See [Security requirements when using credential configurations from an external
  73  	// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
  74  	// for more details.
  75  	ExternalAccount CredType = "external_account"
  76  	// ImpersonatedServiceAccount represents an impersonated service account file type.
  77  	//
  78  	// IMPORTANT:
  79  	// This credential type does not validate the credential configuration. A security
  80  	// risk occurs when a credential configuration configured with malicious urls
  81  	// is used.
  82  	// You should validate credential configurations provided by untrusted sources.
  83  	// See [Security requirements when using credential configurations from an external
  84  	// source] https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
  85  	// for more details.
  86  	ImpersonatedServiceAccount CredType = "impersonated_service_account"
  87  	// GDCHServiceAccount represents a GDCH service account credentials.
  88  	GDCHServiceAccount CredType = "gdch_service_account"
  89  	// ExternalAccountAuthorizedUser represents an external account authorized user credentials.
  90  	ExternalAccountAuthorizedUser CredType = "external_account_authorized_user"
  91  )
  92  
  93  // TokenBindingType specifies the type of binding used when requesting a token
  94  // whether to request a hard-bound token using mTLS or an instance identity
  95  // bound token using ALTS.
  96  type TokenBindingType int
  97  
  98  const (
  99  	// NoBinding specifies that requested tokens are not required to have a
 100  	// binding. This is the default option.
 101  	NoBinding TokenBindingType = iota
 102  	// MTLSHardBinding specifies that a hard-bound token should be requested
 103  	// using an mTLS with S2A channel.
 104  	MTLSHardBinding
 105  	// ALTSHardBinding specifies that an instance identity bound token should
 106  	// be requested using an ALTS channel.
 107  	ALTSHardBinding
 108  )
 109  
 110  // OnGCE reports whether this process is running in Google Cloud.
 111  func OnGCE() bool {
 112  	// TODO(codyoss): once all libs use this auth lib move metadata check here
 113  	return allowOnGCECheck && metadata.OnGCE()
 114  }
 115  
 116  // DetectDefault searches for "Application Default Credentials" and returns
 117  // a credential based on the [DetectOptions] provided.
 118  //
 119  // It looks for credentials in the following places, preferring the first
 120  // location found:
 121  //
 122  //   - A JSON file whose path is specified by the GOOGLE_APPLICATION_CREDENTIALS
 123  //     environment variable. For workload identity federation, refer to
 124  //     https://cloud.google.com/iam/docs/how-to#using-workload-identity-federation
 125  //     on how to generate the JSON configuration file for on-prem/non-Google
 126  //     cloud platforms.
 127  //   - A JSON file in a location known to the gcloud command-line tool. On
 128  //     Windows, this is %APPDATA%/gcloud/application_default_credentials.json. On
 129  //     other systems, $HOME/.config/gcloud/application_default_credentials.json.
 130  //   - On Google Compute Engine, Google App Engine standard second generation
 131  //     runtimes, and Google App Engine flexible environment, it fetches
 132  //     credentials from the metadata server.
 133  //
 134  // Important: If you accept a credential configuration (credential
 135  // JSON/File/Stream) from an external source for authentication to Google
 136  // Cloud Platform, you must validate it before providing it to any Google
 137  // API or library. Providing an unvalidated credential configuration to
 138  // Google APIs can compromise the security of your systems and data. For
 139  // more information, refer to [Validate credential configurations from
 140  // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
 141  func DetectDefault(opts *DetectOptions) (*auth.Credentials, error) {
 142  	if err := opts.validate(); err != nil {
 143  		return nil, err
 144  	}
 145  	trustBoundaryEnabled, err := trustboundary.IsEnabled()
 146  	if err != nil {
 147  		return nil, err
 148  	}
 149  	if len(opts.CredentialsJSON) > 0 {
 150  		return readCredentialsFileJSON(opts.CredentialsJSON, opts)
 151  	}
 152  	if opts.CredentialsFile != "" {
 153  		return readCredentialsFile(opts.CredentialsFile, opts)
 154  	}
 155  	if filename := os.Getenv(credsfile.GoogleAppCredsEnvVar); filename != "" {
 156  		creds, err := readCredentialsFile(filename, opts)
 157  		if err != nil {
 158  			return nil, err
 159  		}
 160  		return creds, nil
 161  	}
 162  
 163  	fileName := credsfile.GetWellKnownFileName()
 164  	if b, err := os.ReadFile(fileName); err == nil {
 165  		return readCredentialsFileJSON(b, opts)
 166  	}
 167  
 168  	if OnGCE() {
 169  		metadataClient := metadata.NewWithOptions(&metadata.Options{
 170  			Logger:           opts.logger(),
 171  			UseDefaultClient: true,
 172  		})
 173  		gceUniverseDomainProvider := &internal.ComputeUniverseDomainProvider{
 174  			MetadataClient: metadataClient,
 175  		}
 176  
 177  		tp := computeTokenProvider(opts, metadataClient)
 178  		if trustBoundaryEnabled {
 179  			gceConfigProvider := trustboundary.NewGCEConfigProvider(gceUniverseDomainProvider)
 180  			var err error
 181  			tp, err = trustboundary.NewProvider(opts.client(), gceConfigProvider, opts.logger(), tp)
 182  			if err != nil {
 183  				return nil, fmt.Errorf("credentials: failed to initialize GCE trust boundary provider: %w", err)
 184  			}
 185  
 186  		}
 187  		return auth.NewCredentials(&auth.CredentialsOptions{
 188  			TokenProvider: tp,
 189  			ProjectIDProvider: auth.CredentialsPropertyFunc(func(ctx context.Context) (string, error) {
 190  				return metadataClient.ProjectIDWithContext(ctx)
 191  			}),
 192  			UniverseDomainProvider: gceUniverseDomainProvider,
 193  		}), nil
 194  	}
 195  
 196  	return nil, fmt.Errorf("credentials: could not find default credentials. See %v for more information", adcSetupURL)
 197  }
 198  
 199  // DetectOptions provides configuration for [DetectDefault].
 200  type DetectOptions struct {
 201  	// Scopes that credentials tokens should have. Example:
 202  	// https://www.googleapis.com/auth/cloud-platform. Required if Audience is
 203  	// not provided.
 204  	Scopes []string
 205  	// TokenBindingType specifies the type of binding used when requesting a
 206  	// token whether to request a hard-bound token using mTLS or an instance
 207  	// identity bound token using ALTS. Optional.
 208  	TokenBindingType TokenBindingType
 209  	// Audience that credentials tokens should have. Only applicable for 2LO
 210  	// flows with service accounts. If specified, scopes should not be provided.
 211  	Audience string
 212  	// Subject is the user email used for [domain wide delegation](https://developers.google.com/identity/protocols/oauth2/service-account#delegatingauthority).
 213  	// Optional.
 214  	Subject string
 215  	// EarlyTokenRefresh configures how early before a token expires that it
 216  	// should be refreshed. Once the token’s time until expiration has entered
 217  	// this refresh window the token is considered valid but stale. If unset,
 218  	// the default value is 3 minutes and 45 seconds. Optional.
 219  	EarlyTokenRefresh time.Duration
 220  	// DisableAsyncRefresh configures a synchronous workflow that refreshes
 221  	// stale tokens while blocking. The default is false. Optional.
 222  	DisableAsyncRefresh bool
 223  	// AuthHandlerOptions configures an authorization handler and other options
 224  	// for 3LO flows. It is required, and only used, for client credential
 225  	// flows.
 226  	AuthHandlerOptions *auth.AuthorizationHandlerOptions
 227  	// TokenURL allows to set the token endpoint for user credential flows. If
 228  	// unset the default value is: https://oauth2.googleapis.com/token.
 229  	// Optional.
 230  	TokenURL string
 231  	// STSAudience is the audience sent to when retrieving an STS token.
 232  	// Currently this only used for GDCH auth flow, for which it is required.
 233  	STSAudience string
 234  	// CredentialsFile overrides detection logic and sources a credential file
 235  	// from the provided filepath. If provided, CredentialsJSON must not be.
 236  	// Optional.
 237  	//
 238  	// Deprecated: This field is deprecated because of a potential security risk.
 239  	// It does not validate the credential configuration. The security risk occurs
 240  	// when a credential configuration is accepted from a source that is not
 241  	// under your control and used without validation on your side.
 242  	//
 243  	// If you know that you will be loading credential configurations of a
 244  	// specific type, it is recommended to use a credential-type-specific
 245  	// NewCredentialsFromFile method. This will ensure that an unexpected
 246  	// credential type with potential for malicious intent is not loaded
 247  	// unintentionally. You might still have to do validation for certain
 248  	// credential types. Please follow the recommendation for that method. For
 249  	// example, if you want to load only service accounts, you can use
 250  	//
 251  	//	creds, err := credentials.NewCredentialsFromFile(ctx, credentials.ServiceAccount, filename, opts)
 252  	//
 253  	// If you are loading your credential configuration from an untrusted source
 254  	// and have not mitigated the risks (e.g. by validating the configuration
 255  	// yourself), make these changes as soon as possible to prevent security
 256  	// risks to your environment.
 257  	//
 258  	// Regardless of the method used, it is always your responsibility to
 259  	// validate configurations received from external sources.
 260  	//
 261  	// For more details see:
 262  	// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
 263  	CredentialsFile string
 264  	// CredentialsJSON overrides detection logic and uses the JSON bytes as the
 265  	// source for the credential. If provided, CredentialsFile must not be.
 266  	// Optional.
 267  	//
 268  	// Deprecated: This field is deprecated because of a potential security risk.
 269  	// It does not validate the credential configuration. The security risk occurs
 270  	// when a credential configuration is accepted from a source that is not
 271  	// under your control and used without validation on your side.
 272  	//
 273  	// If you know that you will be loading credential configurations of a
 274  	// specific type, it is recommended to use a credential-type-specific
 275  	// NewCredentialsFromJSON method. This will ensure that an unexpected
 276  	// credential type with potential for malicious intent is not loaded
 277  	// unintentionally. You might still have to do validation for certain
 278  	// credential types. Please follow the recommendation for that method. For
 279  	// example, if you want to load only service accounts, you can use
 280  	//
 281  	//	creds, err := credentials.NewCredentialsFromJSON(ctx, credentials.ServiceAccount, json, opts)
 282  	//
 283  	// If you are loading your credential configuration from an untrusted source
 284  	// and have not mitigated the risks (e.g. by validating the configuration
 285  	// yourself), make these changes as soon as possible to prevent security
 286  	// risks to your environment.
 287  	//
 288  	// Regardless of the method used, it is always your responsibility to
 289  	// validate configurations received from external sources.
 290  	//
 291  	// For more details see:
 292  	// https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
 293  	CredentialsJSON []byte
 294  	// UseSelfSignedJWT directs service account based credentials to create a
 295  	// self-signed JWT with the private key found in the file, skipping any
 296  	// network requests that would normally be made. Optional.
 297  	UseSelfSignedJWT bool
 298  	// Client configures the underlying client used to make network requests
 299  	// when fetching tokens. Optional.
 300  	Client *http.Client
 301  	// UniverseDomain is the default service domain for a given Cloud universe.
 302  	// The default value is "googleapis.com". This option is ignored for
 303  	// authentication flows that do not support universe domain. Optional.
 304  	UniverseDomain string
 305  	// Logger is used for debug logging. If provided, logging will be enabled
 306  	// at the loggers configured level. By default logging is disabled unless
 307  	// enabled by setting GOOGLE_SDK_GO_LOGGING_LEVEL in which case a default
 308  	// logger will be used. Optional.
 309  	Logger *slog.Logger
 310  }
 311  
 312  // NewCredentialsFromFile creates a [cloud.google.com/go/auth.Credentials] from
 313  // the provided file. The credType argument specifies the expected credential
 314  // type. If the file content does not match the expected type, an error is
 315  // returned.
 316  //
 317  // Important: If you accept a credential configuration (credential
 318  // JSON/File/Stream) from an external source for authentication to Google
 319  // Cloud Platform, you must validate it before providing it to any Google
 320  // API or library. Providing an unvalidated credential configuration to
 321  // Google APIs can compromise the security of your systems and data. For
 322  // more information, refer to [Validate credential configurations from
 323  // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
 324  func NewCredentialsFromFile(credType CredType, filename string, opts *DetectOptions) (*auth.Credentials, error) {
 325  	b, err := os.ReadFile(filename)
 326  	if err != nil {
 327  		return nil, err
 328  	}
 329  	return NewCredentialsFromJSON(credType, b, opts)
 330  }
 331  
 332  // NewCredentialsFromJSON creates a [cloud.google.com/go/auth.Credentials] from
 333  // the provided JSON bytes. The credType argument specifies the expected
 334  // credential type. If the JSON does not match the expected type, an error is
 335  // returned.
 336  //
 337  // Important: If you accept a credential configuration (credential
 338  // JSON/File/Stream) from an external source for authentication to Google
 339  // Cloud Platform, you must validate it before providing it to any Google
 340  // API or library. Providing an unvalidated credential configuration to
 341  // Google APIs can compromise the security of your systems and data. For
 342  // more information, refer to [Validate credential configurations from
 343  // external sources](https://cloud.google.com/docs/authentication/external/externally-sourced-credentials).
 344  func NewCredentialsFromJSON(credType CredType, b []byte, opts *DetectOptions) (*auth.Credentials, error) {
 345  	if err := checkCredentialType(b, credType); err != nil {
 346  		return nil, err
 347  	}
 348  	// We can't use readCredentialsFileJSON because it does auto-detection
 349  	// for client_credentials.json which we don't support here (no type field).
 350  	// Instead, we call fileCredentials just as readCredentialsFileJSON does
 351  	// when it doesn't detect client_credentials.json.
 352  	return fileCredentials(b, opts)
 353  }
 354  
 355  func checkCredentialType(b []byte, expected CredType) error {
 356  
 357  	fileType, err := credsfile.ParseFileType(b)
 358  	if err != nil {
 359  		return err
 360  	}
 361  	if CredType(fileType) != expected {
 362  		return fmt.Errorf("credentials: expected type %q, found %q", expected, fileType)
 363  	}
 364  	return nil
 365  }
 366  
 367  func (o *DetectOptions) validate() error {
 368  	if o == nil {
 369  		return errors.New("credentials: options must be provided")
 370  	}
 371  	if len(o.Scopes) > 0 && o.Audience != "" {
 372  		return errors.New("credentials: both scopes and audience were provided")
 373  	}
 374  	if len(o.CredentialsJSON) > 0 && o.CredentialsFile != "" {
 375  		return errors.New("credentials: both credentials file and JSON were provided")
 376  	}
 377  	return nil
 378  }
 379  
 380  func (o *DetectOptions) tokenURL() string {
 381  	if o.TokenURL != "" {
 382  		return o.TokenURL
 383  	}
 384  	return googleTokenURL
 385  }
 386  
 387  func (o *DetectOptions) scopes() []string {
 388  	scopes := make([]string, len(o.Scopes))
 389  	copy(scopes, o.Scopes)
 390  	return scopes
 391  }
 392  
 393  func (o *DetectOptions) client() *http.Client {
 394  	if o.Client != nil {
 395  		return o.Client
 396  	}
 397  	return internal.DefaultClient()
 398  }
 399  
 400  func (o *DetectOptions) logger() *slog.Logger {
 401  	return internallog.New(o.Logger)
 402  }
 403  
 404  func readCredentialsFile(filename string, opts *DetectOptions) (*auth.Credentials, error) {
 405  	b, err := os.ReadFile(filename)
 406  	if err != nil {
 407  		return nil, err
 408  	}
 409  	return readCredentialsFileJSON(b, opts)
 410  }
 411  
 412  func readCredentialsFileJSON(b []byte, opts *DetectOptions) (*auth.Credentials, error) {
 413  	// attempt to parse jsonData as a Google Developers Console client_credentials.json.
 414  	config := clientCredConfigFromJSON(b, opts)
 415  	if config != nil {
 416  		if config.AuthHandlerOpts == nil {
 417  			return nil, errors.New("credentials: auth handler must be specified for this credential filetype")
 418  		}
 419  		tp, err := auth.New3LOTokenProvider(config)
 420  		if err != nil {
 421  			return nil, err
 422  		}
 423  		return auth.NewCredentials(&auth.CredentialsOptions{
 424  			TokenProvider: tp,
 425  			JSON:          b,
 426  		}), nil
 427  	}
 428  	return fileCredentials(b, opts)
 429  }
 430  
 431  func clientCredConfigFromJSON(b []byte, opts *DetectOptions) *auth.Options3LO {
 432  	var creds credsfile.ClientCredentialsFile
 433  	var c *credsfile.Config3LO
 434  	if err := json.Unmarshal(b, &creds); err != nil {
 435  		return nil
 436  	}
 437  	switch {
 438  	case creds.Web != nil:
 439  		c = creds.Web
 440  	case creds.Installed != nil:
 441  		c = creds.Installed
 442  	default:
 443  		return nil
 444  	}
 445  	if len(c.RedirectURIs) < 1 {
 446  		return nil
 447  	}
 448  	var handleOpts *auth.AuthorizationHandlerOptions
 449  	if opts.AuthHandlerOptions != nil {
 450  		handleOpts = &auth.AuthorizationHandlerOptions{
 451  			Handler:  opts.AuthHandlerOptions.Handler,
 452  			State:    opts.AuthHandlerOptions.State,
 453  			PKCEOpts: opts.AuthHandlerOptions.PKCEOpts,
 454  		}
 455  	}
 456  	return &auth.Options3LO{
 457  		ClientID:         c.ClientID,
 458  		ClientSecret:     c.ClientSecret,
 459  		RedirectURL:      c.RedirectURIs[0],
 460  		Scopes:           opts.scopes(),
 461  		AuthURL:          c.AuthURI,
 462  		TokenURL:         c.TokenURI,
 463  		Client:           opts.client(),
 464  		Logger:           opts.logger(),
 465  		EarlyTokenExpiry: opts.EarlyTokenRefresh,
 466  		AuthHandlerOpts:  handleOpts,
 467  		// TODO(codyoss): refactor this out. We need to add in auto-detection
 468  		// for this use case.
 469  		AuthStyle: auth.StyleInParams,
 470  	}
 471  }
 472