592 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			592 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Go
		
	
	
	
| // 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 auth
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"net/url"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"cloud.google.com/go/auth/internal"
 | |
| 	"cloud.google.com/go/auth/internal/jwt"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	// Parameter keys for AuthCodeURL method to support PKCE.
 | |
| 	codeChallengeKey       = "code_challenge"
 | |
| 	codeChallengeMethodKey = "code_challenge_method"
 | |
| 
 | |
| 	// Parameter key for Exchange method to support PKCE.
 | |
| 	codeVerifierKey = "code_verifier"
 | |
| 
 | |
| 	// 3 minutes and 45 seconds before expiration. The shortest MDS cache is 4 minutes,
 | |
| 	// so we give it 15 seconds to refresh it's cache before attempting to refresh a token.
 | |
| 	defaultExpiryDelta = 225 * time.Second
 | |
| 
 | |
| 	universeDomainDefault = "googleapis.com"
 | |
| )
 | |
| 
 | |
| // tokenState represents different states for a [Token].
 | |
| type tokenState int
 | |
| 
 | |
| const (
 | |
| 	// fresh indicates that the [Token] is valid. It is not expired or close to
 | |
| 	// expired, or the token has no expiry.
 | |
| 	fresh tokenState = iota
 | |
| 	// stale indicates that the [Token] is close to expired, and should be
 | |
| 	// refreshed. The token can be used normally.
 | |
| 	stale
 | |
| 	// invalid indicates that the [Token] is expired or invalid. The token
 | |
| 	// cannot be used for a normal operation.
 | |
| 	invalid
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
 | |
| 	defaultHeader    = &jwt.Header{Algorithm: jwt.HeaderAlgRSA256, Type: jwt.HeaderType}
 | |
| 
 | |
| 	// for testing
 | |
| 	timeNow = time.Now
 | |
| )
 | |
| 
 | |
| // TokenProvider specifies an interface for anything that can return a token.
 | |
| type TokenProvider interface {
 | |
| 	// Token returns a Token or an error.
 | |
| 	// The Token returned must be safe to use
 | |
| 	// concurrently.
 | |
| 	// The returned Token must not be modified.
 | |
| 	// The context provided must be sent along to any requests that are made in
 | |
| 	// the implementing code.
 | |
| 	Token(context.Context) (*Token, error)
 | |
| }
 | |
| 
 | |
| // Token holds the credential token used to authorized requests. All fields are
 | |
| // considered read-only.
 | |
| type Token struct {
 | |
| 	// Value is the token used to authorize requests. It is usually an access
 | |
| 	// token but may be other types of tokens such as ID tokens in some flows.
 | |
| 	Value string
 | |
| 	// Type is the type of token Value is. If uninitialized, it should be
 | |
| 	// assumed to be a "Bearer" token.
 | |
| 	Type string
 | |
| 	// Expiry is the time the token is set to expire.
 | |
| 	Expiry time.Time
 | |
| 	// Metadata  may include, but is not limited to, the body of the token
 | |
| 	// response returned by the server.
 | |
| 	Metadata map[string]interface{} // TODO(codyoss): maybe make a method to flatten metadata to avoid []string for url.Values
 | |
| }
 | |
| 
 | |
| // IsValid reports that a [Token] is non-nil, has a [Token.Value], and has not
 | |
| // expired. A token is considered expired if [Token.Expiry] has passed or will
 | |
| // pass in the next 225 seconds.
 | |
| func (t *Token) IsValid() bool {
 | |
| 	return t.isValidWithEarlyExpiry(defaultExpiryDelta)
 | |
| }
 | |
| 
 | |
| func (t *Token) isValidWithEarlyExpiry(earlyExpiry time.Duration) bool {
 | |
| 	if t.isEmpty() {
 | |
| 		return false
 | |
| 	}
 | |
| 	if t.Expiry.IsZero() {
 | |
| 		return true
 | |
| 	}
 | |
| 	return !t.Expiry.Round(0).Add(-earlyExpiry).Before(timeNow())
 | |
| }
 | |
| 
 | |
| func (t *Token) isEmpty() bool {
 | |
| 	return t == nil || t.Value == ""
 | |
| }
 | |
| 
 | |
| // Credentials holds Google credentials, including
 | |
| // [Application Default Credentials](https://developers.google.com/accounts/docs/application-default-credentials).
 | |
| type Credentials struct {
 | |
| 	json           []byte
 | |
| 	projectID      CredentialsPropertyProvider
 | |
| 	quotaProjectID CredentialsPropertyProvider
 | |
| 	// universeDomain is the default service domain for a given Cloud universe.
 | |
| 	universeDomain CredentialsPropertyProvider
 | |
| 
 | |
| 	TokenProvider
 | |
| }
 | |
| 
 | |
| // JSON returns the bytes associated with the the file used to source
 | |
| // credentials if one was used.
 | |
| func (c *Credentials) JSON() []byte {
 | |
| 	return c.json
 | |
| }
 | |
| 
 | |
| // ProjectID returns the associated project ID from the underlying file or
 | |
| // environment.
 | |
| func (c *Credentials) ProjectID(ctx context.Context) (string, error) {
 | |
| 	if c.projectID == nil {
 | |
| 		return internal.GetProjectID(c.json, ""), nil
 | |
| 	}
 | |
| 	v, err := c.projectID.GetProperty(ctx)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return internal.GetProjectID(c.json, v), nil
 | |
| }
 | |
| 
 | |
| // QuotaProjectID returns the associated quota project ID from the underlying
 | |
| // file or environment.
 | |
| func (c *Credentials) QuotaProjectID(ctx context.Context) (string, error) {
 | |
| 	if c.quotaProjectID == nil {
 | |
| 		return internal.GetQuotaProject(c.json, ""), nil
 | |
| 	}
 | |
| 	v, err := c.quotaProjectID.GetProperty(ctx)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return internal.GetQuotaProject(c.json, v), nil
 | |
| }
 | |
| 
 | |
| // UniverseDomain returns the default service domain for a given Cloud universe.
 | |
| // The default value is "googleapis.com".
 | |
| func (c *Credentials) UniverseDomain(ctx context.Context) (string, error) {
 | |
| 	if c.universeDomain == nil {
 | |
| 		return universeDomainDefault, nil
 | |
| 	}
 | |
| 	v, err := c.universeDomain.GetProperty(ctx)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	if v == "" {
 | |
| 		return universeDomainDefault, nil
 | |
| 	}
 | |
| 	return v, err
 | |
| }
 | |
| 
 | |
| // CredentialsPropertyProvider provides an implementation to fetch a property
 | |
| // value for [Credentials].
 | |
| type CredentialsPropertyProvider interface {
 | |
| 	GetProperty(context.Context) (string, error)
 | |
| }
 | |
| 
 | |
| // CredentialsPropertyFunc is a type adapter to allow the use of ordinary
 | |
| // functions as a [CredentialsPropertyProvider].
 | |
| type CredentialsPropertyFunc func(context.Context) (string, error)
 | |
| 
 | |
| // GetProperty loads the properly value provided the given context.
 | |
| func (p CredentialsPropertyFunc) GetProperty(ctx context.Context) (string, error) {
 | |
| 	return p(ctx)
 | |
| }
 | |
| 
 | |
| // CredentialsOptions are used to configure [Credentials].
 | |
| type CredentialsOptions struct {
 | |
| 	// TokenProvider is a means of sourcing a token for the credentials. Required.
 | |
| 	TokenProvider TokenProvider
 | |
| 	// JSON is the raw contents of the credentials file if sourced from a file.
 | |
| 	JSON []byte
 | |
| 	// ProjectIDProvider resolves the project ID associated with the
 | |
| 	// credentials.
 | |
| 	ProjectIDProvider CredentialsPropertyProvider
 | |
| 	// QuotaProjectIDProvider resolves the quota project ID associated with the
 | |
| 	// credentials.
 | |
| 	QuotaProjectIDProvider CredentialsPropertyProvider
 | |
| 	// UniverseDomainProvider resolves the universe domain with the credentials.
 | |
| 	UniverseDomainProvider CredentialsPropertyProvider
 | |
| }
 | |
| 
 | |
| // NewCredentials returns new [Credentials] from the provided options. Most users
 | |
| // will want to build this object a function from the
 | |
| // [cloud.google.com/go/auth/credentials] package.
 | |
| func NewCredentials(opts *CredentialsOptions) *Credentials {
 | |
| 	creds := &Credentials{
 | |
| 		TokenProvider:  opts.TokenProvider,
 | |
| 		json:           opts.JSON,
 | |
| 		projectID:      opts.ProjectIDProvider,
 | |
| 		quotaProjectID: opts.QuotaProjectIDProvider,
 | |
| 		universeDomain: opts.UniverseDomainProvider,
 | |
| 	}
 | |
| 
 | |
| 	return creds
 | |
| }
 | |
| 
 | |
| // CachedTokenProviderOptions provided options for configuring a
 | |
| // CachedTokenProvider.
 | |
| type CachedTokenProviderOptions struct {
 | |
| 	// DisableAutoRefresh makes the TokenProvider always return the same token,
 | |
| 	// even if it is expired. The default is false. Optional.
 | |
| 	DisableAutoRefresh bool
 | |
| 	// ExpireEarly configures the amount of time before a token expires, that it
 | |
| 	// should be refreshed. If unset, the default value is 3 minutes and 45
 | |
| 	// seconds. Optional.
 | |
| 	ExpireEarly time.Duration
 | |
| 	// DisableAsyncRefresh configures a synchronous workflow that refreshes
 | |
| 	// stale tokens while blocking. The default is false. Optional.
 | |
| 	DisableAsyncRefresh bool
 | |
| }
 | |
| 
 | |
| func (ctpo *CachedTokenProviderOptions) autoRefresh() bool {
 | |
| 	if ctpo == nil {
 | |
| 		return true
 | |
| 	}
 | |
| 	return !ctpo.DisableAutoRefresh
 | |
| }
 | |
| 
 | |
| func (ctpo *CachedTokenProviderOptions) expireEarly() time.Duration {
 | |
| 	if ctpo == nil {
 | |
| 		return defaultExpiryDelta
 | |
| 	}
 | |
| 	return ctpo.ExpireEarly
 | |
| }
 | |
| 
 | |
| func (ctpo *CachedTokenProviderOptions) blockingRefresh() bool {
 | |
| 	if ctpo == nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	return ctpo.DisableAsyncRefresh
 | |
| }
 | |
| 
 | |
| // NewCachedTokenProvider wraps a [TokenProvider] to cache the tokens returned
 | |
| // by the underlying provider. By default it will refresh tokens asynchronously
 | |
| // (non-blocking mode) within a window that starts 3 minutes and 45 seconds
 | |
| // before they expire. The asynchronous (non-blocking) refresh can be changed to
 | |
| // a synchronous (blocking) refresh using the
 | |
| // CachedTokenProviderOptions.DisableAsyncRefresh option. The time-before-expiry
 | |
| // duration can be configured using the CachedTokenProviderOptions.ExpireEarly
 | |
| // option.
 | |
| func NewCachedTokenProvider(tp TokenProvider, opts *CachedTokenProviderOptions) TokenProvider {
 | |
| 	if ctp, ok := tp.(*cachedTokenProvider); ok {
 | |
| 		return ctp
 | |
| 	}
 | |
| 	return &cachedTokenProvider{
 | |
| 		tp:              tp,
 | |
| 		autoRefresh:     opts.autoRefresh(),
 | |
| 		expireEarly:     opts.expireEarly(),
 | |
| 		blockingRefresh: opts.blockingRefresh(),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type cachedTokenProvider struct {
 | |
| 	tp              TokenProvider
 | |
| 	autoRefresh     bool
 | |
| 	expireEarly     time.Duration
 | |
| 	blockingRefresh bool
 | |
| 
 | |
| 	mu          sync.Mutex
 | |
| 	cachedToken *Token
 | |
| 	// isRefreshRunning ensures that the non-blocking refresh will only be
 | |
| 	// attempted once, even if multiple callers enter the Token method.
 | |
| 	isRefreshRunning bool
 | |
| 	// isRefreshErr ensures that the non-blocking refresh will only be attempted
 | |
| 	// once per refresh window if an error is encountered.
 | |
| 	isRefreshErr bool
 | |
| }
 | |
| 
 | |
| func (c *cachedTokenProvider) Token(ctx context.Context) (*Token, error) {
 | |
| 	if c.blockingRefresh {
 | |
| 		return c.tokenBlocking(ctx)
 | |
| 	}
 | |
| 	return c.tokenNonBlocking(ctx)
 | |
| }
 | |
| 
 | |
| func (c *cachedTokenProvider) tokenNonBlocking(ctx context.Context) (*Token, error) {
 | |
| 	switch c.tokenState() {
 | |
| 	case fresh:
 | |
| 		c.mu.Lock()
 | |
| 		defer c.mu.Unlock()
 | |
| 		return c.cachedToken, nil
 | |
| 	case stale:
 | |
| 		c.tokenAsync(ctx)
 | |
| 		// Return the stale token immediately to not block customer requests to Cloud services.
 | |
| 		c.mu.Lock()
 | |
| 		defer c.mu.Unlock()
 | |
| 		return c.cachedToken, nil
 | |
| 	default: // invalid
 | |
| 		return c.tokenBlocking(ctx)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // tokenState reports the token's validity.
 | |
| func (c *cachedTokenProvider) tokenState() tokenState {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	t := c.cachedToken
 | |
| 	if t == nil || t.Value == "" {
 | |
| 		return invalid
 | |
| 	} else if t.Expiry.IsZero() {
 | |
| 		return fresh
 | |
| 	} else if timeNow().After(t.Expiry.Round(0)) {
 | |
| 		return invalid
 | |
| 	} else if timeNow().After(t.Expiry.Round(0).Add(-c.expireEarly)) {
 | |
| 		return stale
 | |
| 	}
 | |
| 	return fresh
 | |
| }
 | |
| 
 | |
| // tokenAsync uses a bool to ensure that only one non-blocking token refresh
 | |
| // happens at a time, even if multiple callers have entered this function
 | |
| // concurrently. This avoids creating an arbitrary number of concurrent
 | |
| // goroutines. Retries should be attempted and managed within the Token method.
 | |
| // If the refresh attempt fails, no further attempts are made until the refresh
 | |
| // window expires and the token enters the invalid state, at which point the
 | |
| // blocking call to Token should likely return the same error on the main goroutine.
 | |
| func (c *cachedTokenProvider) tokenAsync(ctx context.Context) {
 | |
| 	fn := func() {
 | |
| 		c.mu.Lock()
 | |
| 		c.isRefreshRunning = true
 | |
| 		c.mu.Unlock()
 | |
| 		t, err := c.tp.Token(ctx)
 | |
| 		c.mu.Lock()
 | |
| 		defer c.mu.Unlock()
 | |
| 		c.isRefreshRunning = false
 | |
| 		if err != nil {
 | |
| 			// Discard errors from the non-blocking refresh, but prevent further
 | |
| 			// attempts.
 | |
| 			c.isRefreshErr = true
 | |
| 			return
 | |
| 		}
 | |
| 		c.cachedToken = t
 | |
| 	}
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	if !c.isRefreshRunning && !c.isRefreshErr {
 | |
| 		go fn()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (c *cachedTokenProvider) tokenBlocking(ctx context.Context) (*Token, error) {
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	c.isRefreshErr = false
 | |
| 	if c.cachedToken.IsValid() || (!c.autoRefresh && !c.cachedToken.isEmpty()) {
 | |
| 		return c.cachedToken, nil
 | |
| 	}
 | |
| 	t, err := c.tp.Token(ctx)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	c.cachedToken = t
 | |
| 	return t, nil
 | |
| }
 | |
| 
 | |
| // Error is a error associated with retrieving a [Token]. It can hold useful
 | |
| // additional details for debugging.
 | |
| type Error struct {
 | |
| 	// Response is the HTTP response associated with error. The body will always
 | |
| 	// be already closed and consumed.
 | |
| 	Response *http.Response
 | |
| 	// Body is the HTTP response body.
 | |
| 	Body []byte
 | |
| 	// Err is the underlying wrapped error.
 | |
| 	Err error
 | |
| 
 | |
| 	// code returned in the token response
 | |
| 	code string
 | |
| 	// description returned in the token response
 | |
| 	description string
 | |
| 	// uri returned in the token response
 | |
| 	uri string
 | |
| }
 | |
| 
 | |
| func (e *Error) Error() string {
 | |
| 	if e.code != "" {
 | |
| 		s := fmt.Sprintf("auth: %q", e.code)
 | |
| 		if e.description != "" {
 | |
| 			s += fmt.Sprintf(" %q", e.description)
 | |
| 		}
 | |
| 		if e.uri != "" {
 | |
| 			s += fmt.Sprintf(" %q", e.uri)
 | |
| 		}
 | |
| 		return s
 | |
| 	}
 | |
| 	return fmt.Sprintf("auth: cannot fetch token: %v\nResponse: %s", e.Response.StatusCode, e.Body)
 | |
| }
 | |
| 
 | |
| // Temporary returns true if the error is considered temporary and may be able
 | |
| // to be retried.
 | |
| func (e *Error) Temporary() bool {
 | |
| 	if e.Response == nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	sc := e.Response.StatusCode
 | |
| 	return sc == http.StatusInternalServerError || sc == http.StatusServiceUnavailable || sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests
 | |
| }
 | |
| 
 | |
| func (e *Error) Unwrap() error {
 | |
| 	return e.Err
 | |
| }
 | |
| 
 | |
| // Style describes how the token endpoint wants to receive the ClientID and
 | |
| // ClientSecret.
 | |
| type Style int
 | |
| 
 | |
| const (
 | |
| 	// StyleUnknown means the value has not been initiated. Sending this in
 | |
| 	// a request will cause the token exchange to fail.
 | |
| 	StyleUnknown Style = iota
 | |
| 	// StyleInParams sends client info in the body of a POST request.
 | |
| 	StyleInParams
 | |
| 	// StyleInHeader sends client info using Basic Authorization header.
 | |
| 	StyleInHeader
 | |
| )
 | |
| 
 | |
| // Options2LO is the configuration settings for doing a 2-legged JWT OAuth2 flow.
 | |
| type Options2LO struct {
 | |
| 	// Email is the OAuth2 client ID. This value is set as the "iss" in the
 | |
| 	// JWT.
 | |
| 	Email string
 | |
| 	// PrivateKey contains the contents of an RSA private key or the
 | |
| 	// contents of a PEM file that contains a private key. It is used to sign
 | |
| 	// the JWT created.
 | |
| 	PrivateKey []byte
 | |
| 	// TokenURL is th URL the JWT is sent to. Required.
 | |
| 	TokenURL string
 | |
| 	// PrivateKeyID is the ID of the key used to sign the JWT. It is used as the
 | |
| 	// "kid" in the JWT header. Optional.
 | |
| 	PrivateKeyID string
 | |
| 	// Subject is the used for to impersonate a user. It is used as the "sub" in
 | |
| 	// the JWT.m Optional.
 | |
| 	Subject string
 | |
| 	// Scopes specifies requested permissions for the token. Optional.
 | |
| 	Scopes []string
 | |
| 	// Expires specifies the lifetime of the token. Optional.
 | |
| 	Expires time.Duration
 | |
| 	// Audience specifies the "aud" in the JWT. Optional.
 | |
| 	Audience string
 | |
| 	// PrivateClaims allows specifying any custom claims for the JWT. Optional.
 | |
| 	PrivateClaims map[string]interface{}
 | |
| 
 | |
| 	// Client is the client to be used to make the underlying token requests.
 | |
| 	// Optional.
 | |
| 	Client *http.Client
 | |
| 	// UseIDToken requests that the token returned be an ID token if one is
 | |
| 	// returned from the server. Optional.
 | |
| 	UseIDToken bool
 | |
| }
 | |
| 
 | |
| func (o *Options2LO) client() *http.Client {
 | |
| 	if o.Client != nil {
 | |
| 		return o.Client
 | |
| 	}
 | |
| 	return internal.CloneDefaultClient()
 | |
| }
 | |
| 
 | |
| func (o *Options2LO) validate() error {
 | |
| 	if o == nil {
 | |
| 		return errors.New("auth: options must be provided")
 | |
| 	}
 | |
| 	if o.Email == "" {
 | |
| 		return errors.New("auth: email must be provided")
 | |
| 	}
 | |
| 	if len(o.PrivateKey) == 0 {
 | |
| 		return errors.New("auth: private key must be provided")
 | |
| 	}
 | |
| 	if o.TokenURL == "" {
 | |
| 		return errors.New("auth: token URL must be provided")
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // New2LOTokenProvider returns a [TokenProvider] from the provided options.
 | |
| func New2LOTokenProvider(opts *Options2LO) (TokenProvider, error) {
 | |
| 	if err := opts.validate(); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return tokenProvider2LO{opts: opts, Client: opts.client()}, nil
 | |
| }
 | |
| 
 | |
| type tokenProvider2LO struct {
 | |
| 	opts   *Options2LO
 | |
| 	Client *http.Client
 | |
| }
 | |
| 
 | |
| func (tp tokenProvider2LO) Token(ctx context.Context) (*Token, error) {
 | |
| 	pk, err := internal.ParseKey(tp.opts.PrivateKey)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	claimSet := &jwt.Claims{
 | |
| 		Iss:              tp.opts.Email,
 | |
| 		Scope:            strings.Join(tp.opts.Scopes, " "),
 | |
| 		Aud:              tp.opts.TokenURL,
 | |
| 		AdditionalClaims: tp.opts.PrivateClaims,
 | |
| 		Sub:              tp.opts.Subject,
 | |
| 	}
 | |
| 	if t := tp.opts.Expires; t > 0 {
 | |
| 		claimSet.Exp = time.Now().Add(t).Unix()
 | |
| 	}
 | |
| 	if aud := tp.opts.Audience; aud != "" {
 | |
| 		claimSet.Aud = aud
 | |
| 	}
 | |
| 	h := *defaultHeader
 | |
| 	h.KeyID = tp.opts.PrivateKeyID
 | |
| 	payload, err := jwt.EncodeJWS(&h, claimSet, pk)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	v := url.Values{}
 | |
| 	v.Set("grant_type", defaultGrantType)
 | |
| 	v.Set("assertion", payload)
 | |
| 	resp, err := tp.Client.PostForm(tp.opts.TokenURL, v)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
 | |
| 	}
 | |
| 	defer resp.Body.Close()
 | |
| 	body, err := internal.ReadAll(resp.Body)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
 | |
| 	}
 | |
| 	if c := resp.StatusCode; c < http.StatusOK || c >= http.StatusMultipleChoices {
 | |
| 		return nil, &Error{
 | |
| 			Response: resp,
 | |
| 			Body:     body,
 | |
| 		}
 | |
| 	}
 | |
| 	// tokenRes is the JSON response body.
 | |
| 	var tokenRes struct {
 | |
| 		AccessToken string `json:"access_token"`
 | |
| 		TokenType   string `json:"token_type"`
 | |
| 		IDToken     string `json:"id_token"`
 | |
| 		ExpiresIn   int64  `json:"expires_in"`
 | |
| 	}
 | |
| 	if err := json.Unmarshal(body, &tokenRes); err != nil {
 | |
| 		return nil, fmt.Errorf("auth: cannot fetch token: %w", err)
 | |
| 	}
 | |
| 	token := &Token{
 | |
| 		Value: tokenRes.AccessToken,
 | |
| 		Type:  tokenRes.TokenType,
 | |
| 	}
 | |
| 	token.Metadata = make(map[string]interface{})
 | |
| 	json.Unmarshal(body, &token.Metadata) // no error checks for optional fields
 | |
| 
 | |
| 	if secs := tokenRes.ExpiresIn; secs > 0 {
 | |
| 		token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
 | |
| 	}
 | |
| 	if v := tokenRes.IDToken; v != "" {
 | |
| 		// decode returned id token to get expiry
 | |
| 		claimSet, err := jwt.DecodeJWS(v)
 | |
| 		if err != nil {
 | |
| 			return nil, fmt.Errorf("auth: error decoding JWT token: %w", err)
 | |
| 		}
 | |
| 		token.Expiry = time.Unix(claimSet.Exp, 0)
 | |
| 	}
 | |
| 	if tp.opts.UseIDToken {
 | |
| 		if tokenRes.IDToken == "" {
 | |
| 			return nil, fmt.Errorf("auth: response doesn't have JWT token")
 | |
| 		}
 | |
| 		token.Value = tokenRes.IDToken
 | |
| 	}
 | |
| 	return token, nil
 | |
| }
 |