Home | History | Annotate | Download | only in internal
      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 contains support packages for oauth2 package.
      6 package internal
      7 
      8 import (
      9 	"encoding/json"
     10 	"fmt"
     11 	"io"
     12 	"io/ioutil"
     13 	"mime"
     14 	"net/http"
     15 	"net/url"
     16 	"strconv"
     17 	"strings"
     18 	"time"
     19 
     20 	"golang.org/x/net/context"
     21 	"golang.org/x/net/context/ctxhttp"
     22 )
     23 
     24 // Token represents the crendentials 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 in JSON form.
     59 type tokenJSON struct {
     60 	AccessToken  string         `json:"access_token"`
     61 	TokenType    string         `json:"token_type"`
     62 	RefreshToken string         `json:"refresh_token"`
     63 	ExpiresIn    expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
     64 	Expires      expirationTime `json:"expires"`    // broken Facebook spelling of expires_in
     65 }
     66 
     67 func (e *tokenJSON) expiry() (t time.Time) {
     68 	if v := e.ExpiresIn; v != 0 {
     69 		return time.Now().Add(time.Duration(v) * time.Second)
     70 	}
     71 	if v := e.Expires; v != 0 {
     72 		return time.Now().Add(time.Duration(v) * time.Second)
     73 	}
     74 	return
     75 }
     76 
     77 type expirationTime int32
     78 
     79 func (e *expirationTime) UnmarshalJSON(b []byte) error {
     80 	var n json.Number
     81 	err := json.Unmarshal(b, &n)
     82 	if err != nil {
     83 		return err
     84 	}
     85 	i, err := n.Int64()
     86 	if err != nil {
     87 		return err
     88 	}
     89 	*e = expirationTime(i)
     90 	return nil
     91 }
     92 
     93 var brokenAuthHeaderProviders = []string{
     94 	"https://accounts.google.com/",
     95 	"https://api.codeswholesale.com/oauth/token",
     96 	"https://api.dropbox.com/",
     97 	"https://api.dropboxapi.com/",
     98 	"https://api.instagram.com/",
     99 	"https://api.netatmo.net/",
    100 	"https://api.odnoklassniki.ru/",
    101 	"https://api.pushbullet.com/",
    102 	"https://api.soundcloud.com/",
    103 	"https://api.twitch.tv/",
    104 	"https://app.box.com/",
    105 	"https://connect.stripe.com/",
    106 	"https://graph.facebook.com", // see https://github.com/golang/oauth2/issues/214
    107 	"https://login.microsoftonline.com/",
    108 	"https://login.salesforce.com/",
    109 	"https://login.windows.net",
    110 	"https://oauth.sandbox.trainingpeaks.com/",
    111 	"https://oauth.trainingpeaks.com/",
    112 	"https://oauth.vk.com/",
    113 	"https://openapi.baidu.com/",
    114 	"https://slack.com/",
    115 	"https://test-sandbox.auth.corp.google.com",
    116 	"https://test.salesforce.com/",
    117 	"https://user.gini.net/",
    118 	"https://www.douban.com/",
    119 	"https://www.googleapis.com/",
    120 	"https://www.linkedin.com/",
    121 	"https://www.strava.com/oauth/",
    122 	"https://www.wunderlist.com/oauth/",
    123 	"https://api.patreon.com/",
    124 	"https://sandbox.codeswholesale.com/oauth/token",
    125 	"https://api.sipgate.com/v1/authorization/oauth",
    126 }
    127 
    128 // brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints.
    129 var brokenAuthHeaderDomains = []string{
    130 	".force.com",
    131 	".myshopify.com",
    132 	".okta.com",
    133 	".oktapreview.com",
    134 }
    135 
    136 func RegisterBrokenAuthHeaderProvider(tokenURL string) {
    137 	brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL)
    138 }
    139 
    140 // providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
    141 // implements the OAuth2 spec correctly
    142 // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
    143 // In summary:
    144 // - Reddit only accepts client secret in the Authorization header
    145 // - Dropbox accepts either it in URL param or Auth header, but not both.
    146 // - Google only accepts URL param (not spec compliant?), not Auth header
    147 // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
    148 func providerAuthHeaderWorks(tokenURL string) bool {
    149 	for _, s := range brokenAuthHeaderProviders {
    150 		if strings.HasPrefix(tokenURL, s) {
    151 			// Some sites fail to implement the OAuth2 spec fully.
    152 			return false
    153 		}
    154 	}
    155 
    156 	if u, err := url.Parse(tokenURL); err == nil {
    157 		for _, s := range brokenAuthHeaderDomains {
    158 			if strings.HasSuffix(u.Host, s) {
    159 				return false
    160 			}
    161 		}
    162 	}
    163 
    164 	// Assume the provider implements the spec properly
    165 	// otherwise. We can add more exceptions as they're
    166 	// discovered. We will _not_ be adding configurable hooks
    167 	// to this package to let users select server bugs.
    168 	return true
    169 }
    170 
    171 func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) {
    172 	hc, err := ContextClient(ctx)
    173 	if err != nil {
    174 		return nil, err
    175 	}
    176 	bustedAuth := !providerAuthHeaderWorks(tokenURL)
    177 	if bustedAuth {
    178 		if clientID != "" {
    179 			v.Set("client_id", clientID)
    180 		}
    181 		if clientSecret != "" {
    182 			v.Set("client_secret", clientSecret)
    183 		}
    184 	}
    185 	req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
    186 	if err != nil {
    187 		return nil, err
    188 	}
    189 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    190 	if !bustedAuth {
    191 		req.SetBasicAuth(clientID, clientSecret)
    192 	}
    193 	r, err := ctxhttp.Do(ctx, hc, req)
    194 	if err != nil {
    195 		return nil, err
    196 	}
    197 	defer r.Body.Close()
    198 	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
    199 	if err != nil {
    200 		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
    201 	}
    202 	if code := r.StatusCode; code < 200 || code > 299 {
    203 		return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
    204 	}
    205 
    206 	var token *Token
    207 	content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
    208 	switch content {
    209 	case "application/x-www-form-urlencoded", "text/plain":
    210 		vals, err := url.ParseQuery(string(body))
    211 		if err != nil {
    212 			return nil, err
    213 		}
    214 		token = &Token{
    215 			AccessToken:  vals.Get("access_token"),
    216 			TokenType:    vals.Get("token_type"),
    217 			RefreshToken: vals.Get("refresh_token"),
    218 			Raw:          vals,
    219 		}
    220 		e := vals.Get("expires_in")
    221 		if e == "" {
    222 			// TODO(jbd): Facebook's OAuth2 implementation is broken and
    223 			// returns expires_in field in expires. Remove the fallback to expires,
    224 			// when Facebook fixes their implementation.
    225 			e = vals.Get("expires")
    226 		}
    227 		expires, _ := strconv.Atoi(e)
    228 		if expires != 0 {
    229 			token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
    230 		}
    231 	default:
    232 		var tj tokenJSON
    233 		if err = json.Unmarshal(body, &tj); err != nil {
    234 			return nil, err
    235 		}
    236 		token = &Token{
    237 			AccessToken:  tj.AccessToken,
    238 			TokenType:    tj.TokenType,
    239 			RefreshToken: tj.RefreshToken,
    240 			Expiry:       tj.expiry(),
    241 			Raw:          make(map[string]interface{}),
    242 		}
    243 		json.Unmarshal(body, &token.Raw) // no error checks for optional fields
    244 	}
    245 	// Don't overwrite `RefreshToken` with an empty value
    246 	// if this was a token refreshing request.
    247 	if token.RefreshToken == "" {
    248 		token.RefreshToken = v.Get("refresh_token")
    249 	}
    250 	return token, nil
    251 }
    252