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