You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
263 lines
7.6 KiB
263 lines
7.6 KiB
// Copyright 2014 The Go Authors. All rights reserved. |
|
// Use of this source code is governed by a BSD-style |
|
// license that can be found in the LICENSE file. |
|
|
|
package internal |
|
|
|
import ( |
|
"encoding/json" |
|
"errors" |
|
"fmt" |
|
"io" |
|
"io/ioutil" |
|
"mime" |
|
"net/http" |
|
"net/url" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"golang.org/x/net/context" |
|
"golang.org/x/net/context/ctxhttp" |
|
) |
|
|
|
// Token represents the credentials used to authorize |
|
// the requests to access protected resources on the OAuth 2.0 |
|
// provider's backend. |
|
// |
|
// This type is a mirror of oauth2.Token and exists to break |
|
// an otherwise-circular dependency. Other internal packages |
|
// should convert this Token into an oauth2.Token before use. |
|
type Token struct { |
|
// AccessToken is the token that authorizes and authenticates |
|
// the requests. |
|
AccessToken string |
|
|
|
// TokenType is the type of token. |
|
// The Type method returns either this or "Bearer", the default. |
|
TokenType string |
|
|
|
// RefreshToken is a token that's used by the application |
|
// (as opposed to the user) to refresh the access token |
|
// if it expires. |
|
RefreshToken string |
|
|
|
// Expiry is the optional expiration time of the access token. |
|
// |
|
// If zero, TokenSource implementations will reuse the same |
|
// token forever and RefreshToken or equivalent |
|
// mechanisms for that TokenSource will not be used. |
|
Expiry time.Time |
|
|
|
// Raw optionally contains extra metadata from the server |
|
// when updating a token. |
|
Raw interface{} |
|
} |
|
|
|
// tokenJSON is the struct representing the HTTP response from OAuth2 |
|
// providers returning a token in JSON form. |
|
type tokenJSON struct { |
|
AccessToken string `json:"access_token"` |
|
TokenType string `json:"token_type"` |
|
RefreshToken string `json:"refresh_token"` |
|
ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number |
|
Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in |
|
} |
|
|
|
func (e *tokenJSON) expiry() (t time.Time) { |
|
if v := e.ExpiresIn; v != 0 { |
|
return time.Now().Add(time.Duration(v) * time.Second) |
|
} |
|
if v := e.Expires; v != 0 { |
|
return time.Now().Add(time.Duration(v) * time.Second) |
|
} |
|
return |
|
} |
|
|
|
type expirationTime int32 |
|
|
|
func (e *expirationTime) UnmarshalJSON(b []byte) error { |
|
var n json.Number |
|
err := json.Unmarshal(b, &n) |
|
if err != nil { |
|
return err |
|
} |
|
i, err := n.Int64() |
|
if err != nil { |
|
return err |
|
} |
|
*e = expirationTime(i) |
|
return nil |
|
} |
|
|
|
var brokenAuthHeaderProviders = []string{ |
|
"https://accounts.google.com/", |
|
"https://api.codeswholesale.com/oauth/token", |
|
"https://api.dropbox.com/", |
|
"https://api.dropboxapi.com/", |
|
"https://api.instagram.com/", |
|
"https://api.netatmo.net/", |
|
"https://api.odnoklassniki.ru/", |
|
"https://api.pushbullet.com/", |
|
"https://api.soundcloud.com/", |
|
"https://api.twitch.tv/", |
|
"https://app.box.com/", |
|
"https://connect.stripe.com/", |
|
"https://graph.facebook.com", // see https://github.com/golang/oauth2/issues/214 |
|
"https://login.microsoftonline.com/", |
|
"https://login.salesforce.com/", |
|
"https://login.windows.net", |
|
"https://login.live.com/", |
|
"https://oauth.sandbox.trainingpeaks.com/", |
|
"https://oauth.trainingpeaks.com/", |
|
"https://oauth.vk.com/", |
|
"https://openapi.baidu.com/", |
|
"https://slack.com/", |
|
"https://test-sandbox.auth.corp.google.com", |
|
"https://test.salesforce.com/", |
|
"https://user.gini.net/", |
|
"https://www.douban.com/", |
|
"https://www.googleapis.com/", |
|
"https://www.linkedin.com/", |
|
"https://www.strava.com/oauth/", |
|
"https://www.wunderlist.com/oauth/", |
|
"https://api.patreon.com/", |
|
"https://sandbox.codeswholesale.com/oauth/token", |
|
"https://api.sipgate.com/v1/authorization/oauth", |
|
} |
|
|
|
// brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints. |
|
var brokenAuthHeaderDomains = []string{ |
|
".force.com", |
|
".myshopify.com", |
|
".okta.com", |
|
".oktapreview.com", |
|
} |
|
|
|
func RegisterBrokenAuthHeaderProvider(tokenURL string) { |
|
brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL) |
|
} |
|
|
|
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL |
|
// implements the OAuth2 spec correctly |
|
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background. |
|
// In summary: |
|
// - Reddit only accepts client secret in the Authorization header |
|
// - Dropbox accepts either it in URL param or Auth header, but not both. |
|
// - Google only accepts URL param (not spec compliant?), not Auth header |
|
// - Stripe only accepts client secret in Auth header with Bearer method, not Basic |
|
func providerAuthHeaderWorks(tokenURL string) bool { |
|
for _, s := range brokenAuthHeaderProviders { |
|
if strings.HasPrefix(tokenURL, s) { |
|
// Some sites fail to implement the OAuth2 spec fully. |
|
return false |
|
} |
|
} |
|
|
|
if u, err := url.Parse(tokenURL); err == nil { |
|
for _, s := range brokenAuthHeaderDomains { |
|
if strings.HasSuffix(u.Host, s) { |
|
return false |
|
} |
|
} |
|
} |
|
|
|
// Assume the provider implements the spec properly |
|
// otherwise. We can add more exceptions as they're |
|
// discovered. We will _not_ be adding configurable hooks |
|
// to this package to let users select server bugs. |
|
return true |
|
} |
|
|
|
func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) { |
|
bustedAuth := !providerAuthHeaderWorks(tokenURL) |
|
if bustedAuth { |
|
if clientID != "" { |
|
v.Set("client_id", clientID) |
|
} |
|
if clientSecret != "" { |
|
v.Set("client_secret", clientSecret) |
|
} |
|
} |
|
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode())) |
|
if err != nil { |
|
return nil, err |
|
} |
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
|
if !bustedAuth { |
|
req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret)) |
|
} |
|
r, err := ctxhttp.Do(ctx, ContextClient(ctx), req) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer r.Body.Close() |
|
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20)) |
|
if err != nil { |
|
return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err) |
|
} |
|
if code := r.StatusCode; code < 200 || code > 299 { |
|
return nil, &RetrieveError{ |
|
Response: r, |
|
Body: body, |
|
} |
|
} |
|
|
|
var token *Token |
|
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) |
|
switch content { |
|
case "application/x-www-form-urlencoded", "text/plain": |
|
vals, err := url.ParseQuery(string(body)) |
|
if err != nil { |
|
return nil, err |
|
} |
|
token = &Token{ |
|
AccessToken: vals.Get("access_token"), |
|
TokenType: vals.Get("token_type"), |
|
RefreshToken: vals.Get("refresh_token"), |
|
Raw: vals, |
|
} |
|
e := vals.Get("expires_in") |
|
if e == "" { |
|
// TODO(jbd): Facebook's OAuth2 implementation is broken and |
|
// returns expires_in field in expires. Remove the fallback to expires, |
|
// when Facebook fixes their implementation. |
|
e = vals.Get("expires") |
|
} |
|
expires, _ := strconv.Atoi(e) |
|
if expires != 0 { |
|
token.Expiry = time.Now().Add(time.Duration(expires) * time.Second) |
|
} |
|
default: |
|
var tj tokenJSON |
|
if err = json.Unmarshal(body, &tj); err != nil { |
|
return nil, err |
|
} |
|
token = &Token{ |
|
AccessToken: tj.AccessToken, |
|
TokenType: tj.TokenType, |
|
RefreshToken: tj.RefreshToken, |
|
Expiry: tj.expiry(), |
|
Raw: make(map[string]interface{}), |
|
} |
|
json.Unmarshal(body, &token.Raw) // no error checks for optional fields |
|
} |
|
// Don't overwrite `RefreshToken` with an empty value |
|
// if this was a token refreshing request. |
|
if token.RefreshToken == "" { |
|
token.RefreshToken = v.Get("refresh_token") |
|
} |
|
if token.AccessToken == "" { |
|
return token, errors.New("oauth2: server response missing access_token") |
|
} |
|
return token, nil |
|
} |
|
|
|
type RetrieveError struct { |
|
Response *http.Response |
|
Body []byte |
|
} |
|
|
|
func (r *RetrieveError) Error() string { |
|
return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body) |
|
}
|
|
|