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 internal
   6  
   7  import (
   8  	"context"
   9  	"encoding/json"
  10  	"errors"
  11  	"fmt"
  12  	"io"
  13  	"math"
  14  	"mime"
  15  	"net/http"
  16  	"net/url"
  17  	"strconv"
  18  	"strings"
  19  	"sync"
  20  	"sync/atomic"
  21  	"time"
  22  )
  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  // This type is a mirror of [golang.org/x/oauth2.Token] and exists to break
  29  // an otherwise-circular dependency. Other internal packages
  30  // should convert this Token into an [golang.org/x/oauth2.Token] before use.
  31  type Token struct {
  32  	// AccessToken is the token that authorizes and authenticates
  33  	// the requests.
  34  	AccessToken string
  35  
  36  	// TokenType is the type of token.
  37  	// The Type method returns either this or "Bearer", the default.
  38  	TokenType string
  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
  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
  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  
  64  // tokenJSON is the struct representing the HTTP response from OAuth2
  65  // providers returning a token or error in JSON form.
  66  // https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
  67  type tokenJSON struct {
  68  	AccessToken  string         `json:"access_token"`
  69  	TokenType    string         `json:"token_type"`
  70  	RefreshToken string         `json:"refresh_token"`
  71  	ExpiresIn    expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
  72  	// error fields
  73  	// https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
  74  	ErrorCode        string `json:"error"`
  75  	ErrorDescription string `json:"error_description"`
  76  	ErrorURI         string `json:"error_uri"`
  77  }
  78  
  79  func (e *tokenJSON) expiry() (t time.Time) {
  80  	if v := e.ExpiresIn; v != 0 {
  81  		return time.Now().Add(time.Duration(v) * time.Second)
  82  	}
  83  	return
  84  }
  85  
  86  type expirationTime int32
  87  
  88  func (e *expirationTime) UnmarshalJSON(b []byte) error {
  89  	if len(b) == 0 || string(b) == "null" {
  90  		return nil
  91  	}
  92  	var n json.Number
  93  	err := json.Unmarshal(b, &n)
  94  	if err != nil {
  95  		return err
  96  	}
  97  	i, err := n.Int64()
  98  	if err != nil {
  99  		return err
 100  	}
 101  	if i > math.MaxInt32 {
 102  		i = math.MaxInt32
 103  	}
 104  	*e = expirationTime(i)
 105  	return nil
 106  }
 107  
 108  // AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.
 109  type AuthStyle int
 110  
 111  const (
 112  	AuthStyleUnknown  AuthStyle = 0
 113  	AuthStyleInParams AuthStyle = 1
 114  	AuthStyleInHeader AuthStyle = 2
 115  )
 116  
 117  // LazyAuthStyleCache is a backwards compatibility compromise to let Configs
 118  // have a lazily-initialized AuthStyleCache.
 119  //
 120  // The two users of this, oauth2.Config and oauth2/clientcredentials.Config,
 121  // both would ideally just embed an unexported AuthStyleCache but because both
 122  // were historically allowed to be copied by value we can't retroactively add an
 123  // uncopyable Mutex to them.
 124  //
 125  // We could use an atomic.Pointer, but that was added recently enough (in Go
 126  // 1.18) that we'd break Go 1.17 users where the tests as of 2023-08-03
 127  // still pass. By using an atomic.Value, it supports both Go 1.17 and
 128  // copying by value, even if that's not ideal.
 129  type LazyAuthStyleCache struct {
 130  	v atomic.Value // of *AuthStyleCache
 131  }
 132  
 133  func (lc *LazyAuthStyleCache) Get() *AuthStyleCache {
 134  	if c, ok := lc.v.Load().(*AuthStyleCache); ok {
 135  		return c
 136  	}
 137  	c := new(AuthStyleCache)
 138  	if !lc.v.CompareAndSwap(nil, c) {
 139  		c = lc.v.Load().(*AuthStyleCache)
 140  	}
 141  	return c
 142  }
 143  
 144  type authStyleCacheKey struct {
 145  	url      string
 146  	clientID string
 147  }
 148  
 149  // AuthStyleCache is the set of tokenURLs we've successfully used via
 150  // RetrieveToken and which style auth we ended up using.
 151  // It's called a cache, but it doesn't (yet?) shrink. It's expected that
 152  // the set of OAuth2 servers a program contacts over time is fixed and
 153  // small.
 154  type AuthStyleCache struct {
 155  	mu sync.Mutex
 156  	m  map[authStyleCacheKey]AuthStyle
 157  }
 158  
 159  // lookupAuthStyle reports which auth style we last used with tokenURL
 160  // when calling RetrieveToken and whether we have ever done so.
 161  func (c *AuthStyleCache) lookupAuthStyle(tokenURL, clientID string) (style AuthStyle, ok bool) {
 162  	c.mu.Lock()
 163  	defer c.mu.Unlock()
 164  	style, ok = c.m[authStyleCacheKey{tokenURL, clientID}]
 165  	return
 166  }
 167  
 168  // setAuthStyle adds an entry to authStyleCache, documented above.
 169  func (c *AuthStyleCache) setAuthStyle(tokenURL, clientID string, v AuthStyle) {
 170  	c.mu.Lock()
 171  	defer c.mu.Unlock()
 172  	if c.m == nil {
 173  		c.m = make(map[authStyleCacheKey]AuthStyle)
 174  	}
 175  	c.m[authStyleCacheKey{tokenURL, clientID}] = v
 176  }
 177  
 178  // newTokenRequest returns a new *http.Request to retrieve a new token
 179  // from tokenURL using the provided clientID, clientSecret, and POST
 180  // body parameters.
 181  //
 182  // inParams is whether the clientID & clientSecret should be encoded
 183  // as the POST body. An 'inParams' value of true means to send it in
 184  // the POST body (along with any values in v); false means to send it
 185  // in the Authorization header.
 186  func newTokenRequest(tokenURL, clientID, clientSecret string, v url.Values, authStyle AuthStyle) (*http.Request, error) {
 187  	if authStyle == AuthStyleInParams {
 188  		v = cloneURLValues(v)
 189  		if clientID != "" {
 190  			v.Set("client_id", clientID)
 191  		}
 192  		if clientSecret != "" {
 193  			v.Set("client_secret", clientSecret)
 194  		}
 195  	}
 196  	req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
 197  	if err != nil {
 198  		return nil, err
 199  	}
 200  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
 201  	if authStyle == AuthStyleInHeader {
 202  		req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret))
 203  	}
 204  	return req, nil
 205  }
 206  
 207  func cloneURLValues(v url.Values) url.Values {
 208  	v2 := make(url.Values, len(v))
 209  	for k, vv := range v {
 210  		v2[k] = append([]string(nil), vv...)
 211  	}
 212  	return v2
 213  }
 214  
 215  func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle, styleCache *AuthStyleCache) (*Token, error) {
 216  	needsAuthStyleProbe := authStyle == AuthStyleUnknown
 217  	if needsAuthStyleProbe {
 218  		if style, ok := styleCache.lookupAuthStyle(tokenURL, clientID); ok {
 219  			authStyle = style
 220  			needsAuthStyleProbe = false
 221  		} else {
 222  			authStyle = AuthStyleInHeader // the first way we'll try
 223  		}
 224  	}
 225  	req, err := newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
 226  	if err != nil {
 227  		return nil, err
 228  	}
 229  	token, err := doTokenRoundTrip(ctx, req)
 230  	if err != nil && needsAuthStyleProbe {
 231  		// If we get an error, assume the server wants the
 232  		// clientID & clientSecret in a different form.
 233  		// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
 234  		// In summary:
 235  		// - Reddit only accepts client secret in the Authorization header
 236  		// - Dropbox accepts either it in URL param or Auth header, but not both.
 237  		// - Google only accepts URL param (not spec compliant?), not Auth header
 238  		// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
 239  		//
 240  		// We used to maintain a big table in this code of all the sites and which way
 241  		// they went, but maintaining it didn't scale & got annoying.
 242  		// So just try both ways.
 243  		authStyle = AuthStyleInParams // the second way we'll try
 244  		req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
 245  		token, err = doTokenRoundTrip(ctx, req)
 246  	}
 247  	if needsAuthStyleProbe && err == nil {
 248  		styleCache.setAuthStyle(tokenURL, clientID, authStyle)
 249  	}
 250  	// Don't overwrite `RefreshToken` with an empty value
 251  	// if this was a token refreshing request.
 252  	if token != nil && token.RefreshToken == "" {
 253  		token.RefreshToken = v.Get("refresh_token")
 254  	}
 255  	return token, err
 256  }
 257  
 258  func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
 259  	r, err := ContextClient(ctx).Do(req.WithContext(ctx))
 260  	if err != nil {
 261  		return nil, err
 262  	}
 263  	body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
 264  	r.Body.Close()
 265  	if err != nil {
 266  		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
 267  	}
 268  
 269  	failureStatus := r.StatusCode < 200 || r.StatusCode > 299
 270  	retrieveError := &RetrieveError{
 271  		Response: r,
 272  		Body:     body,
 273  		// attempt to populate error detail below
 274  	}
 275  
 276  	var token *Token
 277  	content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
 278  	switch content {
 279  	case "application/x-www-form-urlencoded", "text/plain":
 280  		// some endpoints return a query string
 281  		vals, err := url.ParseQuery(string(body))
 282  		if err != nil {
 283  			if failureStatus {
 284  				return nil, retrieveError
 285  			}
 286  			return nil, fmt.Errorf("oauth2: cannot parse response: %v", err)
 287  		}
 288  		retrieveError.ErrorCode = vals.Get("error")
 289  		retrieveError.ErrorDescription = vals.Get("error_description")
 290  		retrieveError.ErrorURI = vals.Get("error_uri")
 291  		token = &Token{
 292  			AccessToken:  vals.Get("access_token"),
 293  			TokenType:    vals.Get("token_type"),
 294  			RefreshToken: vals.Get("refresh_token"),
 295  			Raw:          vals,
 296  		}
 297  		e := vals.Get("expires_in")
 298  		expires, _ := strconv.Atoi(e)
 299  		if expires != 0 {
 300  			token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
 301  		}
 302  	default:
 303  		var tj tokenJSON
 304  		if err = json.Unmarshal(body, &tj); err != nil {
 305  			if failureStatus {
 306  				return nil, retrieveError
 307  			}
 308  			return nil, fmt.Errorf("oauth2: cannot parse json: %v", err)
 309  		}
 310  		retrieveError.ErrorCode = tj.ErrorCode
 311  		retrieveError.ErrorDescription = tj.ErrorDescription
 312  		retrieveError.ErrorURI = tj.ErrorURI
 313  		token = &Token{
 314  			AccessToken:  tj.AccessToken,
 315  			TokenType:    tj.TokenType,
 316  			RefreshToken: tj.RefreshToken,
 317  			Expiry:       tj.expiry(),
 318  			ExpiresIn:    int64(tj.ExpiresIn),
 319  			Raw:          make(map[string]any),
 320  		}
 321  		json.Unmarshal(body, &token.Raw) // no error checks for optional fields
 322  	}
 323  	// according to spec, servers should respond status 400 in error case
 324  	// https://www.rfc-editor.org/rfc/rfc6749#section-5.2
 325  	// but some unorthodox servers respond 200 in error case
 326  	if failureStatus || retrieveError.ErrorCode != "" {
 327  		return nil, retrieveError
 328  	}
 329  	if token.AccessToken == "" {
 330  		return nil, errors.New("oauth2: server response missing access_token")
 331  	}
 332  	return token, nil
 333  }
 334  
 335  // mirrors oauth2.RetrieveError
 336  type RetrieveError struct {
 337  	Response         *http.Response
 338  	Body             []byte
 339  	ErrorCode        string
 340  	ErrorDescription string
 341  	ErrorURI         string
 342  }
 343  
 344  func (r *RetrieveError) Error() string {
 345  	if r.ErrorCode != "" {
 346  		s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
 347  		if r.ErrorDescription != "" {
 348  			s += fmt.Sprintf(" %q", r.ErrorDescription)
 349  		}
 350  		if r.ErrorURI != "" {
 351  			s += fmt.Sprintf(" %q", r.ErrorURI)
 352  		}
 353  		return s
 354  	}
 355  	return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
 356  }
 357