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