token.go raw

   1  // Copyright 2014 The Go Authors. All rights reserved.
   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 oauth2
   6  
   7  import (
   8  	"context"
   9  	"fmt"
  10  	"net/http"
  11  	"net/url"
  12  	"strconv"
  13  	"strings"
  14  	"time"
  15  
  16  	"golang.org/x/oauth2/internal"
  17  )
  18  
  19  // defaultExpiryDelta determines how earlier a token should be considered
  20  // expired than its actual expiration time. It is used to avoid late
  21  // expirations due to client-server time mismatches.
  22  const defaultExpiryDelta = 10 * time.Second
  23  
  24  // Token represents the credentials used to authorize
  25  // the requests to access protected resources on the OAuth 2.0
  26  // provider's backend.
  27  //
  28  // Most users of this package should not access fields of Token
  29  // directly. They're exported mostly for use by related packages
  30  // implementing derivative OAuth2 flows.
  31  type Token struct {
  32  	// AccessToken is the token that authorizes and authenticates
  33  	// the requests.
  34  	AccessToken string `json:"access_token"`
  35  
  36  	// TokenType is the type of token.
  37  	// The Type method returns either this or "Bearer", the default.
  38  	TokenType string `json:"token_type,omitempty"`
  39  
  40  	// RefreshToken is a token that's used by the application
  41  	// (as opposed to the user) to refresh the access token
  42  	// if it expires.
  43  	RefreshToken string `json:"refresh_token,omitempty"`
  44  
  45  	// Expiry is the optional expiration time of the access token.
  46  	//
  47  	// If zero, [TokenSource] implementations will reuse the same
  48  	// token forever and RefreshToken or equivalent
  49  	// mechanisms for that TokenSource will not be used.
  50  	Expiry time.Time `json:"expiry,omitempty"`
  51  
  52  	// ExpiresIn is the OAuth2 wire format "expires_in" field,
  53  	// which specifies how many seconds later the token expires,
  54  	// relative to an unknown time base approximately around "now".
  55  	// It is the application's responsibility to populate
  56  	// `Expiry` from `ExpiresIn` when required.
  57  	ExpiresIn int64 `json:"expires_in,omitempty"`
  58  
  59  	// raw optionally contains extra metadata from the server
  60  	// when updating a token.
  61  	raw any
  62  
  63  	// expiryDelta is used to calculate when a token is considered
  64  	// expired, by subtracting from Expiry. If zero, defaultExpiryDelta
  65  	// is used.
  66  	expiryDelta time.Duration
  67  }
  68  
  69  // Type returns t.TokenType if non-empty, else "Bearer".
  70  func (t *Token) Type() string {
  71  	if strings.EqualFold(t.TokenType, "bearer") {
  72  		return "Bearer"
  73  	}
  74  	if strings.EqualFold(t.TokenType, "mac") {
  75  		return "MAC"
  76  	}
  77  	if strings.EqualFold(t.TokenType, "basic") {
  78  		return "Basic"
  79  	}
  80  	if t.TokenType != "" {
  81  		return t.TokenType
  82  	}
  83  	return "Bearer"
  84  }
  85  
  86  // SetAuthHeader sets the Authorization header to r using the access
  87  // token in t.
  88  //
  89  // This method is unnecessary when using [Transport] or an HTTP Client
  90  // returned by this package.
  91  func (t *Token) SetAuthHeader(r *http.Request) {
  92  	r.Header.Set("Authorization", t.Type()+" "+t.AccessToken)
  93  }
  94  
  95  // WithExtra returns a new [Token] that's a clone of t, but using the
  96  // provided raw extra map. This is only intended for use by packages
  97  // implementing derivative OAuth2 flows.
  98  func (t *Token) WithExtra(extra any) *Token {
  99  	t2 := new(Token)
 100  	*t2 = *t
 101  	t2.raw = extra
 102  	return t2
 103  }
 104  
 105  // Extra returns an extra field.
 106  // Extra fields are key-value pairs returned by the server as
 107  // part of the token retrieval response.
 108  func (t *Token) Extra(key string) any {
 109  	if raw, ok := t.raw.(map[string]any); ok {
 110  		return raw[key]
 111  	}
 112  
 113  	vals, ok := t.raw.(url.Values)
 114  	if !ok {
 115  		return nil
 116  	}
 117  
 118  	v := vals.Get(key)
 119  	switch s := strings.TrimSpace(v); strings.Count(s, ".") {
 120  	case 0: // Contains no "."; try to parse as int
 121  		if i, err := strconv.ParseInt(s, 10, 64); err == nil {
 122  			return i
 123  		}
 124  	case 1: // Contains a single "."; try to parse as float
 125  		if f, err := strconv.ParseFloat(s, 64); err == nil {
 126  			return f
 127  		}
 128  	}
 129  
 130  	return v
 131  }
 132  
 133  // timeNow is time.Now but pulled out as a variable for tests.
 134  var timeNow = time.Now
 135  
 136  // expired reports whether the token is expired.
 137  // t must be non-nil.
 138  func (t *Token) expired() bool {
 139  	if t.Expiry.IsZero() {
 140  		return false
 141  	}
 142  
 143  	expiryDelta := defaultExpiryDelta
 144  	if t.expiryDelta != 0 {
 145  		expiryDelta = t.expiryDelta
 146  	}
 147  	return t.Expiry.Round(0).Add(-expiryDelta).Before(timeNow())
 148  }
 149  
 150  // Valid reports whether t is non-nil, has an AccessToken, and is not expired.
 151  func (t *Token) Valid() bool {
 152  	return t != nil && t.AccessToken != "" && !t.expired()
 153  }
 154  
 155  // tokenFromInternal maps an *internal.Token struct into
 156  // a *Token struct.
 157  func tokenFromInternal(t *internal.Token) *Token {
 158  	if t == nil {
 159  		return nil
 160  	}
 161  	return &Token{
 162  		AccessToken:  t.AccessToken,
 163  		TokenType:    t.TokenType,
 164  		RefreshToken: t.RefreshToken,
 165  		Expiry:       t.Expiry,
 166  		ExpiresIn:    t.ExpiresIn,
 167  		raw:          t.Raw,
 168  	}
 169  }
 170  
 171  // retrieveToken takes a *Config and uses that to retrieve an *internal.Token.
 172  // This token is then mapped from *internal.Token into an *oauth2.Token which is returned along
 173  // with an error.
 174  func retrieveToken(ctx context.Context, c *Config, v url.Values) (*Token, error) {
 175  	tk, err := internal.RetrieveToken(ctx, c.ClientID, c.ClientSecret, c.Endpoint.TokenURL, v, internal.AuthStyle(c.Endpoint.AuthStyle), c.authStyleCache.Get())
 176  	if err != nil {
 177  		if rErr, ok := err.(*internal.RetrieveError); ok {
 178  			return nil, (*RetrieveError)(rErr)
 179  		}
 180  		return nil, err
 181  	}
 182  	return tokenFromInternal(tk), nil
 183  }
 184  
 185  // RetrieveError is the error returned when the token endpoint returns a
 186  // non-2XX HTTP status code or populates RFC 6749's 'error' parameter.
 187  // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
 188  type RetrieveError struct {
 189  	Response *http.Response
 190  	// Body is the body that was consumed by reading Response.Body.
 191  	// It may be truncated.
 192  	Body []byte
 193  	// ErrorCode is RFC 6749's 'error' parameter.
 194  	ErrorCode string
 195  	// ErrorDescription is RFC 6749's 'error_description' parameter.
 196  	ErrorDescription string
 197  	// ErrorURI is RFC 6749's 'error_uri' parameter.
 198  	ErrorURI string
 199  }
 200  
 201  func (r *RetrieveError) Error() string {
 202  	if r.ErrorCode != "" {
 203  		s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
 204  		if r.ErrorDescription != "" {
 205  			s += fmt.Sprintf(" %q", r.ErrorDescription)
 206  		}
 207  		if r.ErrorURI != "" {
 208  			s += fmt.Sprintf(" %q", r.ErrorURI)
 209  		}
 210  		return s
 211  	}
 212  	return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
 213  }
 214