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.
6package internal
7
8import (
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)
22
23// Token represents the crendentials used to authorize
24// the requests to access protected resources on the OAuth 2.0
25// provider's backend.
26//
27// This type is a mirror of oauth2.Token and exists to break
28// an otherwise-circular dependency. Other internal packages
29// should convert this Token into an oauth2.Token before use.
30type Token struct {
31	// AccessToken is the token that authorizes and authenticates
32	// the requests.
33	AccessToken string
34
35	// TokenType is the type of token.
36	// The Type method returns either this or "Bearer", the default.
37	TokenType string
38
39	// RefreshToken is a token that's used by the application
40	// (as opposed to the user) to refresh the access token
41	// if it expires.
42	RefreshToken string
43
44	// Expiry is the optional expiration time of the access token.
45	//
46	// If zero, TokenSource implementations will reuse the same
47	// token forever and RefreshToken or equivalent
48	// mechanisms for that TokenSource will not be used.
49	Expiry time.Time
50
51	// Raw optionally contains extra metadata from the server
52	// when updating a token.
53	Raw interface{}
54}
55
56// tokenJSON is the struct representing the HTTP response from OAuth2
57// providers returning a token in JSON form.
58type tokenJSON struct {
59	AccessToken  string         `json:"access_token"`
60	TokenType    string         `json:"token_type"`
61	RefreshToken string         `json:"refresh_token"`
62	ExpiresIn    expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
63	Expires      expirationTime `json:"expires"`    // broken Facebook spelling of expires_in
64}
65
66func (e *tokenJSON) expiry() (t time.Time) {
67	if v := e.ExpiresIn; v != 0 {
68		return time.Now().Add(time.Duration(v) * time.Second)
69	}
70	if v := e.Expires; v != 0 {
71		return time.Now().Add(time.Duration(v) * time.Second)
72	}
73	return
74}
75
76type expirationTime int32
77
78func (e *expirationTime) UnmarshalJSON(b []byte) error {
79	var n json.Number
80	err := json.Unmarshal(b, &n)
81	if err != nil {
82		return err
83	}
84	i, err := n.Int64()
85	if err != nil {
86		return err
87	}
88	*e = expirationTime(i)
89	return nil
90}
91
92var brokenAuthHeaderProviders = []string{
93	"https://accounts.google.com/",
94	"https://api.dropbox.com/",
95	"https://api.instagram.com/",
96	"https://api.netatmo.net/",
97	"https://api.odnoklassniki.ru/",
98	"https://api.pushbullet.com/",
99	"https://api.soundcloud.com/",
100	"https://api.twitch.tv/",
101	"https://app.box.com/",
102	"https://connect.stripe.com/",
103	"https://login.microsoftonline.com/",
104	"https://login.salesforce.com/",
105	"https://oauth.sandbox.trainingpeaks.com/",
106	"https://oauth.trainingpeaks.com/",
107	"https://oauth.vk.com/",
108	"https://openapi.baidu.com/",
109	"https://slack.com/",
110	"https://test-sandbox.auth.corp.google.com",
111	"https://test.salesforce.com/",
112	"https://user.gini.net/",
113	"https://www.douban.com/",
114	"https://www.googleapis.com/",
115	"https://www.linkedin.com/",
116	"https://www.strava.com/oauth/",
117	"https://www.wunderlist.com/oauth/",
118	"https://api.patreon.com/",
119}
120
121func RegisterBrokenAuthHeaderProvider(tokenURL string) {
122	brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL)
123}
124
125// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
126// implements the OAuth2 spec correctly
127// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
128// In summary:
129// - Reddit only accepts client secret in the Authorization header
130// - Dropbox accepts either it in URL param or Auth header, but not both.
131// - Google only accepts URL param (not spec compliant?), not Auth header
132// - Stripe only accepts client secret in Auth header with Bearer method, not Basic
133func providerAuthHeaderWorks(tokenURL string) bool {
134	for _, s := range brokenAuthHeaderProviders {
135		if strings.HasPrefix(tokenURL, s) {
136			// Some sites fail to implement the OAuth2 spec fully.
137			return false
138		}
139	}
140
141	// Assume the provider implements the spec properly
142	// otherwise. We can add more exceptions as they're
143	// discovered. We will _not_ be adding configurable hooks
144	// to this package to let users select server bugs.
145	return true
146}
147
148func RetrieveToken(ctx context.Context, ClientID, ClientSecret, TokenURL string, v url.Values) (*Token, error) {
149	hc, err := ContextClient(ctx)
150	if err != nil {
151		return nil, err
152	}
153	v.Set("client_id", ClientID)
154	bustedAuth := !providerAuthHeaderWorks(TokenURL)
155	if bustedAuth && ClientSecret != "" {
156		v.Set("client_secret", ClientSecret)
157	}
158	req, err := http.NewRequest("POST", TokenURL, strings.NewReader(v.Encode()))
159	if err != nil {
160		return nil, err
161	}
162	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
163	if !bustedAuth {
164		req.SetBasicAuth(ClientID, ClientSecret)
165	}
166	r, err := hc.Do(req)
167	if err != nil {
168		return nil, err
169	}
170	defer r.Body.Close()
171	body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
172	if err != nil {
173		return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
174	}
175	if code := r.StatusCode; code < 200 || code > 299 {
176		return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
177	}
178
179	var token *Token
180	content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
181	switch content {
182	case "application/x-www-form-urlencoded", "text/plain":
183		vals, err := url.ParseQuery(string(body))
184		if err != nil {
185			return nil, err
186		}
187		token = &Token{
188			AccessToken:  vals.Get("access_token"),
189			TokenType:    vals.Get("token_type"),
190			RefreshToken: vals.Get("refresh_token"),
191			Raw:          vals,
192		}
193		e := vals.Get("expires_in")
194		if e == "" {
195			// TODO(jbd): Facebook's OAuth2 implementation is broken and
196			// returns expires_in field in expires. Remove the fallback to expires,
197			// when Facebook fixes their implementation.
198			e = vals.Get("expires")
199		}
200		expires, _ := strconv.Atoi(e)
201		if expires != 0 {
202			token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
203		}
204	default:
205		var tj tokenJSON
206		if err = json.Unmarshal(body, &tj); err != nil {
207			return nil, err
208		}
209		token = &Token{
210			AccessToken:  tj.AccessToken,
211			TokenType:    tj.TokenType,
212			RefreshToken: tj.RefreshToken,
213			Expiry:       tj.expiry(),
214			Raw:          make(map[string]interface{}),
215		}
216		json.Unmarshal(body, &token.Raw) // no error checks for optional fields
217	}
218	// Don't overwrite `RefreshToken` with an empty value
219	// if this was a token refreshing request.
220	if token.RefreshToken == "" {
221		token.RefreshToken = v.Get("refresh_token")
222	}
223	return token, nil
224}
225