gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

token.go (10251B)


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