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