creds.go raw

   1  // Copyright 2017 Google LLC.
   2  // Use of this source code is governed by a BSD-style
   3  // license that can be found in the LICENSE file.
   4  
   5  package internal
   6  
   7  import (
   8  	"context"
   9  	"crypto/tls"
  10  	"encoding/json"
  11  	"errors"
  12  	"fmt"
  13  	"net"
  14  	"net/http"
  15  	"os"
  16  	"time"
  17  
  18  	"cloud.google.com/go/auth"
  19  	"cloud.google.com/go/auth/credentials"
  20  	"cloud.google.com/go/auth/oauth2adapt"
  21  	"golang.org/x/oauth2"
  22  	"google.golang.org/api/internal/cert"
  23  	"google.golang.org/api/internal/credentialstype"
  24  	"google.golang.org/api/internal/impersonate"
  25  
  26  	"golang.org/x/oauth2/google"
  27  )
  28  
  29  const quotaProjectEnvVar = "GOOGLE_CLOUD_QUOTA_PROJECT"
  30  
  31  // Creds returns credential information obtained from DialSettings, or if none, then
  32  // it returns default credential information.
  33  func Creds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
  34  	if ds.IsNewAuthLibraryEnabled() {
  35  		return credsNewAuth(ds)
  36  	}
  37  	creds, err := baseCreds(ctx, ds)
  38  	if err != nil {
  39  		return nil, err
  40  	}
  41  	if ds.ImpersonationConfig != nil {
  42  		return impersonateCredentials(ctx, creds, ds)
  43  	}
  44  	return creds, nil
  45  }
  46  
  47  // AuthCreds returns [cloud.google.com/go/auth.Credentials] based on credentials
  48  // options provided via [option.ClientOption], including legacy oauth2/google
  49  // options. If there are no applicable options, then it returns the result of
  50  // [cloud.google.com/go/auth/credentials.DetectDefault].
  51  // Note: If NoAuth is true, when [google.golang.org/api/option.WithoutAuthentication]
  52  // is passed, then no authentication will be performed and this function will
  53  // return nil, nil.
  54  func AuthCreds(ctx context.Context, settings *DialSettings) (*auth.Credentials, error) {
  55  	if settings.NoAuth {
  56  		return nil, nil
  57  	}
  58  	if settings.AuthCredentials != nil {
  59  		return settings.AuthCredentials, nil
  60  	}
  61  	// Support oauth2/google options
  62  	var oauth2Creds *google.Credentials
  63  	if settings.InternalCredentials != nil {
  64  		oauth2Creds = settings.InternalCredentials
  65  	} else if settings.Credentials != nil {
  66  		oauth2Creds = settings.Credentials
  67  	} else if settings.TokenSource != nil {
  68  		oauth2Creds = &google.Credentials{TokenSource: settings.TokenSource}
  69  	}
  70  	if oauth2Creds != nil {
  71  		return oauth2adapt.AuthCredentialsFromOauth2Credentials(oauth2Creds), nil
  72  	}
  73  
  74  	return detectDefaultFromDialSettings(settings)
  75  }
  76  
  77  // GetOAuth2Configuration determines configurations for the OAuth2 transport, which is separate from the API transport.
  78  // The OAuth2 transport and endpoint will be configured for mTLS if applicable.
  79  func GetOAuth2Configuration(ctx context.Context, settings *DialSettings) (string, *http.Client, error) {
  80  	clientCertSource, err := getClientCertificateSource(settings)
  81  	if err != nil {
  82  		return "", nil, err
  83  	}
  84  	tokenURL := oAuth2Endpoint(clientCertSource)
  85  	var oauth2Client *http.Client
  86  	if clientCertSource != nil {
  87  		tlsConfig := &tls.Config{
  88  			GetClientCertificate: clientCertSource,
  89  		}
  90  		oauth2Client = customHTTPClient(tlsConfig)
  91  	} else {
  92  		oauth2Client = oauth2.NewClient(ctx, nil)
  93  	}
  94  	return tokenURL, oauth2Client, nil
  95  }
  96  
  97  func credsNewAuth(settings *DialSettings) (*google.Credentials, error) {
  98  	// Preserve old options behavior
  99  	if settings.InternalCredentials != nil {
 100  		return settings.InternalCredentials, nil
 101  	} else if settings.Credentials != nil {
 102  		return settings.Credentials, nil
 103  	} else if settings.TokenSource != nil {
 104  		return &google.Credentials{TokenSource: settings.TokenSource}, nil
 105  	}
 106  
 107  	if settings.AuthCredentials != nil {
 108  		return oauth2adapt.Oauth2CredentialsFromAuthCredentials(settings.AuthCredentials), nil
 109  	}
 110  
 111  	creds, err := detectDefaultFromDialSettings(settings)
 112  	if err != nil {
 113  		return nil, err
 114  	}
 115  	return oauth2adapt.Oauth2CredentialsFromAuthCredentials(creds), nil
 116  }
 117  
 118  func detectDefaultFromDialSettings(settings *DialSettings) (*auth.Credentials, error) {
 119  	var useSelfSignedJWT bool
 120  	var aud string
 121  	var scopes []string
 122  	// If scoped JWTs are enabled user provided an aud, allow self-signed JWT.
 123  	if settings.EnableJwtWithScope || len(settings.Audiences) > 0 {
 124  		useSelfSignedJWT = true
 125  	}
 126  
 127  	if len(settings.Scopes) > 0 {
 128  		scopes = make([]string, len(settings.Scopes))
 129  		copy(scopes, settings.Scopes)
 130  	}
 131  	if len(settings.Audiences) > 0 {
 132  		aud = settings.Audiences[0]
 133  	}
 134  	// Only default scopes if user did not also set an audience.
 135  	if len(settings.Scopes) == 0 && aud == "" && len(settings.DefaultScopes) > 0 {
 136  		scopes = make([]string, len(settings.DefaultScopes))
 137  		copy(scopes, settings.DefaultScopes)
 138  	}
 139  	if len(scopes) == 0 && aud == "" {
 140  		aud = settings.DefaultAudience
 141  	}
 142  
 143  	credsFile, _ := settings.GetAuthCredentialsFile()
 144  	credsJSON, _ := settings.GetAuthCredentialsJSON()
 145  	return credentials.DetectDefault(&credentials.DetectOptions{
 146  		Scopes:           scopes,
 147  		Audience:         aud,
 148  		CredentialsFile:  credsFile,
 149  		CredentialsJSON:  credsJSON,
 150  		UseSelfSignedJWT: useSelfSignedJWT,
 151  		Logger:           settings.Logger,
 152  	})
 153  }
 154  
 155  func baseCreds(ctx context.Context, ds *DialSettings) (*google.Credentials, error) {
 156  	if ds.InternalCredentials != nil {
 157  		return ds.InternalCredentials, nil
 158  	}
 159  	if ds.Credentials != nil {
 160  		return ds.Credentials, nil
 161  	}
 162  	if credsJSON, checkCredType := ds.GetAuthCredentialsJSON(); len(credsJSON) > 0 {
 163  		return credentialsFromJSON(ctx, credsJSON, ds, checkCredType)
 164  	}
 165  	if credsFile, checkCredType := ds.GetAuthCredentialsFile(); credsFile != "" {
 166  		data, err := os.ReadFile(credsFile)
 167  		if err != nil {
 168  			return nil, fmt.Errorf("cannot read credentials file: %v", err)
 169  		}
 170  		return credentialsFromJSON(ctx, data, ds, checkCredType)
 171  	}
 172  	if ds.TokenSource != nil {
 173  		return &google.Credentials{TokenSource: ds.TokenSource}, nil
 174  	}
 175  	cred, err := google.FindDefaultCredentials(ctx, ds.GetScopes()...)
 176  	if err != nil {
 177  		return nil, err
 178  	}
 179  	if len(cred.JSON) > 0 {
 180  		return credentialsFromJSON(ctx, cred.JSON, ds, credentialstype.Unknown)
 181  	}
 182  	// For GAE and GCE, the JSON is empty so return the default credentials directly.
 183  	return cred, nil
 184  }
 185  
 186  // JSON key file type.
 187  const (
 188  	serviceAccountKey = "service_account"
 189  )
 190  
 191  // credentialsFromJSON returns a google.Credentials from the JSON data
 192  //
 193  // - A self-signed JWT flow will be executed if the following conditions are
 194  // met:
 195  //
 196  //	(1) At least one of the following is true:
 197  //	    (a) Scope for self-signed JWT flow is enabled
 198  //	    (b) Audiences are explicitly provided by users
 199  //	(2) No service account impersontation
 200  //
 201  // - Otherwise, executes standard OAuth 2.0 flow
 202  // More details: google.aip.dev/auth/4111
 203  func credentialsFromJSON(ctx context.Context, data []byte, ds *DialSettings, checkCredType credentialstype.CredType) (*google.Credentials, error) {
 204  	if checkCredType != credentialstype.Unknown {
 205  		if err := credentialstype.CheckCredentialType(data, checkCredType); err != nil {
 206  			return nil, err
 207  		}
 208  	}
 209  	var params google.CredentialsParams
 210  	params.Scopes = ds.GetScopes()
 211  
 212  	tokenURL, oauth2Client, err := GetOAuth2Configuration(ctx, ds)
 213  	if err != nil {
 214  		return nil, err
 215  	}
 216  	params.TokenURL = tokenURL
 217  	ctx = context.WithValue(ctx, oauth2.HTTPClient, oauth2Client)
 218  
 219  	// By default, a standard OAuth 2.0 token source is created
 220  	cred, err := google.CredentialsFromJSONWithParams(ctx, data, params)
 221  	if err != nil {
 222  		return nil, err
 223  	}
 224  
 225  	// Override the token source to use self-signed JWT if conditions are met
 226  	isJWTFlow, err := isSelfSignedJWTFlow(data, ds)
 227  	if err != nil {
 228  		return nil, err
 229  	}
 230  	if isJWTFlow {
 231  		ts, err := selfSignedJWTTokenSource(data, ds)
 232  		if err != nil {
 233  			return nil, err
 234  		}
 235  		cred.TokenSource = ts
 236  	}
 237  
 238  	return cred, err
 239  }
 240  
 241  func oAuth2Endpoint(clientCertSource cert.Source) string {
 242  	if isMTLS(clientCertSource) {
 243  		return google.MTLSTokenURL
 244  	}
 245  	return google.Endpoint.TokenURL
 246  }
 247  
 248  func isSelfSignedJWTFlow(data []byte, ds *DialSettings) (bool, error) {
 249  	// For non-GDU universe domains, token exchange is impossible and services
 250  	// must support self-signed JWTs with scopes.
 251  	if !ds.IsUniverseDomainGDU() {
 252  		return typeServiceAccount(data)
 253  	}
 254  	if (ds.EnableJwtWithScope || ds.HasCustomAudience()) && ds.ImpersonationConfig == nil {
 255  		return typeServiceAccount(data)
 256  	}
 257  	return false, nil
 258  }
 259  
 260  // typeServiceAccount checks if JSON data is for a service account.
 261  func typeServiceAccount(data []byte) (bool, error) {
 262  	var f struct {
 263  		Type string `json:"type"`
 264  		// The remaining JSON fields are omitted because they are not used.
 265  	}
 266  	if err := json.Unmarshal(data, &f); err != nil {
 267  		return false, err
 268  	}
 269  	return f.Type == serviceAccountKey, nil
 270  }
 271  
 272  func selfSignedJWTTokenSource(data []byte, ds *DialSettings) (oauth2.TokenSource, error) {
 273  	if len(ds.GetScopes()) > 0 && !ds.HasCustomAudience() {
 274  		// Scopes are preferred in self-signed JWT unless the scope is not available
 275  		// or a custom audience is used.
 276  		return google.JWTAccessTokenSourceWithScope(data, ds.GetScopes()...)
 277  	} else if ds.GetAudience() != "" {
 278  		// Fallback to audience if scope is not provided
 279  		return google.JWTAccessTokenSourceFromJSON(data, ds.GetAudience())
 280  	} else {
 281  		return nil, errors.New("neither scopes or audience are available for the self-signed JWT")
 282  	}
 283  }
 284  
 285  // GetQuotaProject retrieves quota project with precedence being: client option,
 286  // environment variable, creds file.
 287  func GetQuotaProject(creds *google.Credentials, clientOpt string) string {
 288  	if clientOpt != "" {
 289  		return clientOpt
 290  	}
 291  	if env := os.Getenv(quotaProjectEnvVar); env != "" {
 292  		return env
 293  	}
 294  	if creds == nil {
 295  		return ""
 296  	}
 297  	var v struct {
 298  		QuotaProject string `json:"quota_project_id"`
 299  	}
 300  	if err := json.Unmarshal(creds.JSON, &v); err != nil {
 301  		return ""
 302  	}
 303  	return v.QuotaProject
 304  }
 305  
 306  func impersonateCredentials(ctx context.Context, creds *google.Credentials, ds *DialSettings) (*google.Credentials, error) {
 307  	if len(ds.ImpersonationConfig.Scopes) == 0 {
 308  		ds.ImpersonationConfig.Scopes = ds.GetScopes()
 309  	}
 310  	ts, err := impersonate.TokenSource(ctx, creds.TokenSource, ds.ImpersonationConfig)
 311  	if err != nil {
 312  		return nil, err
 313  	}
 314  	return &google.Credentials{
 315  		TokenSource: ts,
 316  		ProjectID:   creds.ProjectID,
 317  	}, nil
 318  }
 319  
 320  // customHTTPClient constructs an HTTPClient using the provided tlsConfig, to support mTLS.
 321  func customHTTPClient(tlsConfig *tls.Config) *http.Client {
 322  	trans := baseTransport()
 323  	trans.TLSClientConfig = tlsConfig
 324  	return &http.Client{Transport: trans}
 325  }
 326  
 327  func baseTransport() *http.Transport {
 328  	return &http.Transport{
 329  		Proxy: http.ProxyFromEnvironment,
 330  		DialContext: (&net.Dialer{
 331  			Timeout:   30 * time.Second,
 332  			KeepAlive: 30 * time.Second,
 333  			DualStack: true,
 334  		}).DialContext,
 335  		MaxIdleConns:          100,
 336  		MaxIdleConnsPerHost:   100,
 337  		IdleConnTimeout:       90 * time.Second,
 338  		TLSHandshakeTimeout:   10 * time.Second,
 339  		ExpectContinueTimeout: 1 * time.Second,
 340  	}
 341  }
 342