...

Source file src/google.golang.org/api/internal/creds.go

Documentation: google.golang.org/api/internal

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

View as plain text