...

Source file src/cloud.google.com/go/auth/auth.go

Documentation: cloud.google.com/go/auth

     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 auth
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"net/http"
    23  	"net/url"
    24  	"strings"
    25  	"sync"
    26  	"time"
    27  
    28  	"cloud.google.com/go/auth/internal"
    29  	"cloud.google.com/go/auth/internal/jwt"
    30  )
    31  
    32  const (
    33  	// Parameter keys for AuthCodeURL method to support PKCE.
    34  	codeChallengeKey       = "code_challenge"
    35  	codeChallengeMethodKey = "code_challenge_method"
    36  
    37  	// Parameter key for Exchange method to support PKCE.
    38  	codeVerifierKey = "code_verifier"
    39  
    40  	// 3 minutes and 45 seconds before expiration. The shortest MDS cache is 4 minutes,
    41  	// so we give it 15 seconds to refresh it's cache before attempting to refresh a token.
    42  	defaultExpiryDelta = 215 * time.Second
    43  
    44  	universeDomainDefault = "googleapis.com"
    45  )
    46  
    47  var (
    48  	defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
    49  	defaultHeader    = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
    50  
    51  	// for testing
    52  	timeNow = time.Now
    53  )
    54  
    55  // TokenProvider specifies an interface for anything that can return a token.
    56  type TokenProvider interface {
    57  	// Token returns a Token or an error.
    58  	// The Token returned must be safe to use
    59  	// concurrently.
    60  	// The returned Token must not be modified.
    61  	// The context provided must be sent along to any requests that are made in
    62  	// the implementing code.
    63  	Token(context.Context) (*Token, error)
    64  }
    65  
    66  // Token holds the credential token used to authorized requests. All fields are
    67  // considered read-only.
    68  type Token struct {
    69  	// Value is the token used to authorize requests. It is usually an access
    70  	// token but may be other types of tokens such as ID tokens in some flows.
    71  	Value string
    72  	// Type is the type of token Value is. If uninitialized, it should be
    73  	// assumed to be a "Bearer" token.
    74  	Type string
    75  	// Expiry is the time the token is set to expire.
    76  	Expiry time.Time
    77  	// Metadata  may include, but is not limited to, the body of the token
    78  	// response returned by the server.
    79  	Metadata map[string]interface{} // TODO(codyoss): maybe make a method to flatten metadata to avoid []string for url.Values
    80  }
    81  
    82  // IsValid reports that a [Token] is non-nil, has a [Token.Value], and has not
    83  // expired. A token is considered expired if [Token.Expiry] has passed or will
    84  // pass in the next 10 seconds.
    85  func (t *Token) IsValid() bool {
    86  	return t.isValidWithEarlyExpiry(defaultExpiryDelta)
    87  }
    88  
    89  func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
    90  	if t == nil || t.Value == "" {
    91  		return false
    92  	}
    93  	if t.Expiry.IsZero() {
    94  		return true
    95  	}
    96  	return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
    97  }
    98  
    99  // Credentials holds Google credentials, including
   100  // [Application Default Credentials](https://developers.google.com/accounts/docs/application-default-credentials).
   101  type Credentials struct {
   102  	json           []byte
   103  	projectID      CredentialsPropertyProvider
   104  	quotaProjectID CredentialsPropertyProvider
   105  	// universeDomain is the default service domain for a given Cloud universe.
   106  	universeDomain CredentialsPropertyProvider
   107  
   108  	TokenProvider
   109  }
   110  
   111  // JSON returns the bytes associated with the the file used to source
   112  // credentials if one was used.
   113  func (c *Credentials) JSON() []byte {
   114  	return c.json
   115  }
   116  
   117  // ProjectID returns the associated project ID from the underlying file or
   118  // environment.
   119  func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
   120  	if c.projectID == nil {
   121  		return internal.GetProjectID(c.json, ""), nil
   122  	}
   123  	v, err := c.projectID.GetProperty(ctx)
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  	return internal.GetProjectID(c.json, v), nil
   128  }
   129  
   130  // QuotaProjectID returns the associated quota project ID from the underlying
   131  // file or environment.
   132  func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
   133  	if c.quotaProjectID == nil {
   134  		return internal.GetQuotaProject(c.json, ""), nil
   135  	}
   136  	v, err := c.quotaProjectID.GetProperty(ctx)
   137  	if err != nil {
   138  		return "", err
   139  	}
   140  	return internal.GetQuotaProject(c.json, v), nil
   141  }
   142  
   143  // UniverseDomain returns the default service domain for a given Cloud universe.
   144  // The default value is "googleapis.com".
   145  func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
   146  	if c.universeDomain == nil {
   147  		return universeDomainDefault, nil
   148  	}
   149  	v, err := c.universeDomain.GetProperty(ctx)
   150  	if err != nil {
   151  		return "", err
   152  	}
   153  	if v == "" {
   154  		return universeDomainDefault, nil
   155  	}
   156  	return v, err
   157  }
   158  
   159  // CredentialsPropertyProvider provides an implementation to fetch a property
   160  // value for [Credentials].
   161  type CredentialsPropertyProvider interface {
   162  	GetProperty(context.Context) (string, error)
   163  }
   164  
   165  // CredentialsPropertyFunc is a type adapter to allow the use of ordinary
   166  // functions as a [CredentialsPropertyProvider].
   167  type CredentialsPropertyFunc func(context.Context) (string, error)
   168  
   169  // GetProperty loads the properly value provided the given context.
   170  func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
   171  	return p(ctx)
   172  }
   173  
   174  // CredentialsOptions are used to configure [Credentials].
   175  type CredentialsOptions struct {
   176  	// TokenProvider is a means of sourcing a token for the credentials. Required.
   177  	TokenProvider TokenProvider
   178  	// JSON is the raw contents of the credentials file if sourced from a file.
   179  	JSON []byte
   180  	// ProjectIDProvider resolves the project ID associated with the
   181  	// credentials.
   182  	ProjectIDProvider CredentialsPropertyProvider
   183  	// QuotaProjectIDProvider resolves the quota project ID associated with the
   184  	// credentials.
   185  	QuotaProjectIDProvider CredentialsPropertyProvider
   186  	// UniverseDomainProvider resolves the universe domain with the credentials.
   187  	UniverseDomainProvider CredentialsPropertyProvider
   188  }
   189  
   190  // NewCredentials returns new [Credentials] from the provided options. Most users
   191  // will want to build this object a function from the
   192  // [cloud.google.com/go/auth/credentials] package.
   193  func NewCredentials(opts *CredentialsOptions) *Credentials {
   194  	creds := &Credentials{
   195  		TokenProvider:  opts.TokenProvider,
   196  		json:           opts.JSON,
   197  		projectID:      opts.ProjectIDProvider,
   198  		quotaProjectID: opts.QuotaProjectIDProvider,
   199  		universeDomain: opts.UniverseDomainProvider,
   200  	}
   201  
   202  	return creds
   203  }
   204  
   205  // CachedTokenProviderOptions provided options for configuring a
   206  // CachedTokenProvider.
   207  type CachedTokenProviderOptions struct {
   208  	// DisableAutoRefresh makes the TokenProvider always return the same token,
   209  	// even if it is expired.
   210  	DisableAutoRefresh bool
   211  	// ExpireEarly configures the amount of time before a token expires, that it
   212  	// should be refreshed. If unset, the default value is 10 seconds.
   213  	ExpireEarly time.Duration
   214  }
   215  
   216  func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
   217  	if ctpo == nil {
   218  		return true
   219  	}
   220  	return !ctpo.DisableAutoRefresh
   221  }
   222  
   223  func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
   224  	if ctpo == nil {
   225  		return defaultExpiryDelta
   226  	}
   227  	return ctpo.ExpireEarly
   228  }
   229  
   230  // NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
   231  // by the underlying provider. By default it will refresh tokens ten seconds
   232  // before they expire, but this time can be configured with the optional
   233  // options.
   234  func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
   235  	if ctp, ok := tp.(*cachedTokenProvider); ok {
   236  		return ctp
   237  	}
   238  	return &cachedTokenProvider{
   239  		tp:          tp,
   240  		autoRefresh: opts.autoRefresh(),
   241  		expireEarly: opts.expireEarly(),
   242  	}
   243  }
   244  
   245  type cachedTokenProvider struct {
   246  	tp          TokenProvider
   247  	autoRefresh bool
   248  	expireEarly time.Duration
   249  
   250  	mu          sync.Mutex
   251  	cachedToken *Token
   252  }
   253  
   254  func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
   255  	c.mu.Lock()
   256  	defer c.mu.Unlock()
   257  	if c.cachedToken.IsValid() || !c.autoRefresh {
   258  		return c.cachedToken, nil
   259  	}
   260  	t, err := c.tp.Token(ctx)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  	c.cachedToken = t
   265  	return t, nil
   266  }
   267  
   268  // Error is a error associated with retrieving a [Token]. It can hold useful
   269  // additional details for debugging.
   270  type Error struct {
   271  	// Response is the HTTP response associated with error. The body will always
   272  	// be already closed and consumed.
   273  	Response *http.Response
   274  	// Body is the HTTP response body.
   275  	Body []byte
   276  	// Err is the underlying wrapped error.
   277  	Err error
   278  
   279  	// code returned in the token response
   280  	code string
   281  	// description returned in the token response
   282  	description string
   283  	// uri returned in the token response
   284  	uri string
   285  }
   286  
   287  func (e *Error) Error() string {
   288  	if e.code != "" {
   289  		s := fmt.Sprintf("auth: %q", e.code)
   290  		if e.description != "" {
   291  			s += fmt.Sprintf(" %q", e.description)
   292  		}
   293  		if e.uri != "" {
   294  			s += fmt.Sprintf(" %q", e.uri)
   295  		}
   296  		return s
   297  	}
   298  	return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
   299  }
   300  
   301  // Temporary returns true if the error is considered temporary and may be able
   302  // to be retried.
   303  func (e *Error) Temporary() bool {
   304  	if e.Response == nil {
   305  		return false
   306  	}
   307  	sc := e.Response.StatusCode
   308  	return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
   309  }
   310  
   311  func (e *Error) Unwrap() error {
   312  	return e.Err
   313  }
   314  
   315  // Style describes how the token endpoint wants to receive the ClientID and
   316  // ClientSecret.
   317  type Style int
   318  
   319  const (
   320  	// StyleUnknown means the value has not been initiated. Sending this in
   321  	// a request will cause the token exchange to fail.
   322  	StyleUnknown Style = iota
   323  	// StyleInParams sends client info in the body of a POST request.
   324  	StyleInParams
   325  	// StyleInHeader sends client info using Basic Authorization header.
   326  	StyleInHeader
   327  )
   328  
   329  // Options2LO is the configuration settings for doing a 2-legged JWT OAuth2 flow.
   330  type Options2LO struct {
   331  	// Email is the OAuth2 client ID. This value is set as the "iss" in the
   332  	// JWT.
   333  	Email string
   334  	// PrivateKey contains the contents of an RSA private key or the
   335  	// contents of a PEM file that contains a private key. It is used to sign
   336  	// the JWT created.
   337  	PrivateKey []byte
   338  	// TokenURL is th URL the JWT is sent to. Required.
   339  	TokenURL string
   340  	// PrivateKeyID is the ID of the key used to sign the JWT. It is used as the
   341  	// "kid" in the JWT header. Optional.
   342  	PrivateKeyID string
   343  	// Subject is the used for to impersonate a user. It is used as the "sub" in
   344  	// the JWT.m Optional.
   345  	Subject string
   346  	// Scopes specifies requested permissions for the token. Optional.
   347  	Scopes []string
   348  	// Expires specifies the lifetime of the token. Optional.
   349  	Expires time.Duration
   350  	// Audience specifies the "aud" in the JWT. Optional.
   351  	Audience string
   352  	// PrivateClaims allows specifying any custom claims for the JWT. Optional.
   353  	PrivateClaims map[string]interface{}
   354  
   355  	// Client is the client to be used to make the underlying token requests.
   356  	// Optional.
   357  	Client *http.Client
   358  	// UseIDToken requests that the token returned be an ID token if one is
   359  	// returned from the server. Optional.
   360  	UseIDToken bool
   361  }
   362  
   363  func (o *Options2LO) client() *http.Client {
   364  	if o.Client != nil {
   365  		return o.Client
   366  	}
   367  	return internal.CloneDefaultClient()
   368  }
   369  
   370  func (o *Options2LO) validate() error {
   371  	if o == nil {
   372  		return errors.New("auth: options must be provided")
   373  	}
   374  	if o.Email == "" {
   375  		return errors.New("auth: email must be provided")
   376  	}
   377  	if len(o.PrivateKey) == 0 {
   378  		return errors.New("auth: private key must be provided")
   379  	}
   380  	if o.TokenURL == "" {
   381  		return errors.New("auth: token URL must be provided")
   382  	}
   383  	return nil
   384  }
   385  
   386  // New2LOTokenProvider returns a [TokenProvider] from the provided options.
   387  func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
   388  	if err := opts.validate(); err != nil {
   389  		return nil, err
   390  	}
   391  	return tokenProvider2LO{opts: opts, Client: opts.client()}, nil
   392  }
   393  
   394  type tokenProvider2LO struct {
   395  	opts   *Options2LO
   396  	Client *http.Client
   397  }
   398  
   399  func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
   400  	pk, err := internal.ParseKey(tp.opts.PrivateKey)
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  	claimSet := &jwt.Claims{
   405  		Iss:              tp.opts.Email,
   406  		Scope:            strings.Join(tp.opts.Scopes, " "),
   407  		Aud:              tp.opts.TokenURL,
   408  		AdditionalClaims: tp.opts.PrivateClaims,
   409  		Sub:              tp.opts.Subject,
   410  	}
   411  	if t := tp.opts.Expires; t > 0 {
   412  		claimSet.Exp = time.Now().Add(t).Unix()
   413  	}
   414  	if aud := tp.opts.Audience; aud != "" {
   415  		claimSet.Aud = aud
   416  	}
   417  	h := *defaultHeader
   418  	h.KeyID = tp.opts.PrivateKeyID
   419  	payload, err := jwt.EncodeJWS(&h, claimSet, pk)
   420  	if err != nil {
   421  		return nil, err
   422  	}
   423  	v := url.Values{}
   424  	v.Set("grant_type", defaultGrantType)
   425  	v.Set("assertion", payload)
   426  	resp, err := tp.Client.PostForm(tp.opts.TokenURL, v)
   427  	if err != nil {
   428  		return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
   429  	}
   430  	defer resp.Body.Close()
   431  	body, err := internal.ReadAll(resp.Body)
   432  	if err != nil {
   433  		return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
   434  	}
   435  	if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
   436  		return nil, &Error{
   437  			Response: resp,
   438  			Body:     body,
   439  		}
   440  	}
   441  	// tokenRes is the JSON response body.
   442  	var tokenRes struct {
   443  		AccessToken string `json:"access_token"`
   444  		TokenType   string `json:"token_type"`
   445  		IDToken     string `json:"id_token"`
   446  		ExpiresIn   int64  `json:"expires_in"`
   447  	}
   448  	if err := json.Unmarshal(body, &tokenRes); err != nil {
   449  		return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
   450  	}
   451  	token := &Token{
   452  		Value: tokenRes.AccessToken,
   453  		Type:  tokenRes.TokenType,
   454  	}
   455  	token.Metadata = make(map[string]interface{})
   456  	json.Unmarshal(body, &token.Metadata) // no error checks for optional fields
   457  
   458  	if secs := tokenRes.ExpiresIn; secs > 0 {
   459  		token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
   460  	}
   461  	if v := tokenRes.IDToken; v != "" {
   462  		// decode returned id token to get expiry
   463  		claimSet, err := jwt.DecodeJWS(v)
   464  		if err != nil {
   465  			return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
   466  		}
   467  		token.Expiry = time.Unix(claimSet.Exp, 0)
   468  	}
   469  	if tp.opts.UseIDToken {
   470  		if tokenRes.IDToken == "" {
   471  			return nil, fmt.Errorf("auth: response doesn't have JWT token")
   472  		}
   473  		token.Value = tokenRes.IDToken
   474  	}
   475  	return token, nil
   476  }
   477  

View as plain text