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	"encoding/json"
9	"errors"
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 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.
31type 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.
59type 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
67func (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
77type expirationTime int32
78
79func (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
93var 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://login.mailchimp.com/",
107	"https://login.microsoftonline.com/",
108	"https://login.salesforce.com/",
109	"https://login.windows.net",
110	"https://login.live.com/",
111	"https://oauth.sandbox.trainingpeaks.com/",
112	"https://oauth.trainingpeaks.com/",
113	"https://oauth.vk.com/",
114	"https://openapi.baidu.com/",
115	"https://slack.com/",
116	"https://test-sandbox.auth.corp.google.com",
117	"https://test.salesforce.com/",
118	"https://user.gini.net/",
119	"https://www.douban.com/",
120	"https://www.googleapis.com/",
121	"https://www.linkedin.com/",
122	"https://www.strava.com/oauth/",
123	"https://www.wunderlist.com/oauth/",
124	"https://api.patreon.com/",
125	"https://sandbox.codeswholesale.com/oauth/token",
126	"https://api.sipgate.com/v1/authorization/oauth",
127	"https://api.medium.com/v1/tokens",
128	"https://log.finalsurge.com/oauth/token",
129	"https://multisport.todaysplan.com.au/rest/oauth/access_token",
130	"https://whats.todaysplan.com.au/rest/oauth/access_token",
131}
132
133// brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints.
134var brokenAuthHeaderDomains = []string{
135	".auth0.com",
136	".force.com",
137	".myshopify.com",
138	".okta.com",
139	".oktapreview.com",
140}
141
142func RegisterBrokenAuthHeaderProvider(tokenURL string) {
143	brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL)
144}
145
146// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
147// implements the OAuth2 spec correctly
148// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
149// In summary:
150// - Reddit only accepts client secret in the Authorization header
151// - Dropbox accepts either it in URL param or Auth header, but not both.
152// - Google only accepts URL param (not spec compliant?), not Auth header
153// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
154func providerAuthHeaderWorks(tokenURL string) bool {
155	for _, s := range brokenAuthHeaderProviders {
156		if strings.HasPrefix(tokenURL, s) {
157			// Some sites fail to implement the OAuth2 spec fully.
158			return false
159		}
160	}
161
162	if u, err := url.Parse(tokenURL); err == nil {
163		for _, s := range brokenAuthHeaderDomains {
164			if strings.HasSuffix(u.Host, s) {
165				return false
166			}
167		}
168	}
169
170	// Assume the provider implements the spec properly
171	// otherwise. We can add more exceptions as they're
172	// discovered. We will _not_ be adding configurable hooks
173	// to this package to let users select server bugs.
174	return true
175}
176
177func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) {
178	bustedAuth := !providerAuthHeaderWorks(tokenURL)
179	if bustedAuth {
180		if clientID != "" {
181			v.Set("client_id", clientID)
182		}
183		if clientSecret != "" {
184			v.Set("client_secret", clientSecret)
185		}
186	}
187	req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
188	if err != nil {
189		return nil, err
190	}
191	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
192	if !bustedAuth {
193		req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret))
194	}
195	r, err := ctxhttp.Do(ctx, ContextClient(ctx), req)
196	if err != nil {
197		return nil, err
198	}
199	defer r.Body.Close()
200	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
201	if err != nil {
202		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
203	}
204	if code := r.StatusCode; code < 200 || code > 299 {
205		return nil, &RetrieveError{
206			Response: r,
207			Body:     body,
208		}
209	}
210
211	var token *Token
212	content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
213	switch content {
214	case "application/x-www-form-urlencoded", "text/plain":
215		vals, err := url.ParseQuery(string(body))
216		if err != nil {
217			return nil, err
218		}
219		token = &Token{
220			AccessToken:  vals.Get("access_token"),
221			TokenType:    vals.Get("token_type"),
222			RefreshToken: vals.Get("refresh_token"),
223			Raw:          vals,
224		}
225		e := vals.Get("expires_in")
226		if e == "" {
227			// TODO(jbd): Facebook's OAuth2 implementation is broken and
228			// returns expires_in field in expires. Remove the fallback to expires,
229			// when Facebook fixes their implementation.
230			e = vals.Get("expires")
231		}
232		expires, _ := strconv.Atoi(e)
233		if expires != 0 {
234			token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
235		}
236	default:
237		var tj tokenJSON
238		if err = json.Unmarshal(body, &tj); err != nil {
239			return nil, err
240		}
241		token = &Token{
242			AccessToken:  tj.AccessToken,
243			TokenType:    tj.TokenType,
244			RefreshToken: tj.RefreshToken,
245			Expiry:       tj.expiry(),
246			Raw:          make(map[string]interface{}),
247		}
248		json.Unmarshal(body, &token.Raw) // no error checks for optional fields
249	}
250	// Don't overwrite `RefreshToken` with an empty value
251	// if this was a token refreshing request.
252	if token.RefreshToken == "" {
253		token.RefreshToken = v.Get("refresh_token")
254	}
255	if token.AccessToken == "" {
256		return token, errors.New("oauth2: server response missing access_token")
257	}
258	return token, nil
259}
260
261type RetrieveError struct {
262	Response *http.Response
263	Body     []byte
264}
265
266func (r *RetrieveError) Error() string {
267	return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
268}
269