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 }