blob: b4723fcacea76822398fb526d329f99e9d57f163 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001// 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"
Zack Williamse940c7a2019-08-21 14:25:39 -070022)
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}
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 return
71}
72
73type expirationTime int32
74
75func (e *expirationTime) UnmarshalJSON(b []byte) error {
76 if len(b) == 0 || string(b) == "null" {
77 return nil
78 }
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 if i > math.MaxInt32 {
89 i = math.MaxInt32
90 }
91 *e = expirationTime(i)
92 return nil
93}
94
95// RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op.
96//
97// Deprecated: this function no longer does anything. Caller code that
98// wants to avoid potential extra HTTP requests made during
99// auto-probing of the provider's auth style should set
100// Endpoint.AuthStyle.
101func RegisterBrokenAuthHeaderProvider(tokenURL string) {}
102
103// AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.
104type AuthStyle int
105
106const (
107 AuthStyleUnknown AuthStyle = 0
108 AuthStyleInParams AuthStyle = 1
109 AuthStyleInHeader AuthStyle = 2
110)
111
112// authStyleCache is the set of tokenURLs we've successfully used via
113// RetrieveToken and which style auth we ended up using.
114// It's called a cache, but it doesn't (yet?) shrink. It's expected that
115// the set of OAuth2 servers a program contacts over time is fixed and
116// small.
117var authStyleCache struct {
118 sync.Mutex
119 m map[string]AuthStyle // keyed by tokenURL
120}
121
122// ResetAuthCache resets the global authentication style cache used
123// for AuthStyleUnknown token requests.
124func ResetAuthCache() {
125 authStyleCache.Lock()
126 defer authStyleCache.Unlock()
127 authStyleCache.m = nil
128}
129
130// lookupAuthStyle reports which auth style we last used with tokenURL
131// when calling RetrieveToken and whether we have ever done so.
132func lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
133 authStyleCache.Lock()
134 defer authStyleCache.Unlock()
135 style, ok = authStyleCache.m[tokenURL]
136 return
137}
138
139// setAuthStyle adds an entry to authStyleCache, documented above.
140func setAuthStyle(tokenURL string, v AuthStyle) {
141 authStyleCache.Lock()
142 defer authStyleCache.Unlock()
143 if authStyleCache.m == nil {
144 authStyleCache.m = make(map[string]AuthStyle)
145 }
146 authStyleCache.m[tokenURL] = v
147}
148
149// newTokenRequest returns a new *http.Request to retrieve a new token
150// from tokenURL using the provided clientID, clientSecret, and POST
151// body parameters.
152//
153// inParams is whether the clientID & clientSecret should be encoded
154// as the POST body. An 'inParams' value of true means to send it in
155// the POST body (along with any values in v); false means to send it
156// in the Authorization header.
157func newTokenRequest(tokenURL, clientID, clientSecret string, v url.Values, authStyle AuthStyle) (*http.Request, error) {
158 if authStyle == AuthStyleInParams {
159 v = cloneURLValues(v)
160 if clientID != "" {
161 v.Set("client_id", clientID)
162 }
163 if clientSecret != "" {
164 v.Set("client_secret", clientSecret)
165 }
166 }
167 req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
168 if err != nil {
169 return nil, err
170 }
171 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
172 if authStyle == AuthStyleInHeader {
173 req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret))
174 }
175 return req, nil
176}
177
178func cloneURLValues(v url.Values) url.Values {
179 v2 := make(url.Values, len(v))
180 for k, vv := range v {
181 v2[k] = append([]string(nil), vv...)
182 }
183 return v2
184}
185
186func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle) (*Token, error) {
187 needsAuthStyleProbe := authStyle == 0
188 if needsAuthStyleProbe {
189 if style, ok := lookupAuthStyle(tokenURL); ok {
190 authStyle = style
191 needsAuthStyleProbe = false
192 } else {
193 authStyle = AuthStyleInHeader // the first way we'll try
194 }
195 }
196 req, err := newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
197 if err != nil {
198 return nil, err
199 }
200 token, err := doTokenRoundTrip(ctx, req)
201 if err != nil && needsAuthStyleProbe {
202 // If we get an error, assume the server wants the
203 // clientID & clientSecret in a different form.
204 // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
205 // In summary:
206 // - Reddit only accepts client secret in the Authorization header
207 // - Dropbox accepts either it in URL param or Auth header, but not both.
208 // - Google only accepts URL param (not spec compliant?), not Auth header
209 // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
210 //
211 // We used to maintain a big table in this code of all the sites and which way
212 // they went, but maintaining it didn't scale & got annoying.
213 // So just try both ways.
214 authStyle = AuthStyleInParams // the second way we'll try
215 req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
216 token, err = doTokenRoundTrip(ctx, req)
217 }
218 if needsAuthStyleProbe && err == nil {
219 setAuthStyle(tokenURL, authStyle)
220 }
221 // Don't overwrite `RefreshToken` with an empty value
222 // if this was a token refreshing request.
223 if token != nil && token.RefreshToken == "" {
224 token.RefreshToken = v.Get("refresh_token")
225 }
226 return token, err
227}
228
229func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
Akash Reddy Kankanalac0014632025-05-21 17:12:20 +0530230 r, err := ContextClient(ctx).Do(req.WithContext(ctx))
Zack Williamse940c7a2019-08-21 14:25:39 -0700231 if err != nil {
232 return nil, err
233 }
234 body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
235 r.Body.Close()
236 if err != nil {
237 return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
238 }
239 if code := r.StatusCode; code < 200 || code > 299 {
240 return nil, &RetrieveError{
241 Response: r,
242 Body: body,
243 }
244 }
245
246 var token *Token
247 content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
248 switch content {
249 case "application/x-www-form-urlencoded", "text/plain":
250 vals, err := url.ParseQuery(string(body))
251 if err != nil {
252 return nil, err
253 }
254 token = &Token{
255 AccessToken: vals.Get("access_token"),
256 TokenType: vals.Get("token_type"),
257 RefreshToken: vals.Get("refresh_token"),
258 Raw: vals,
259 }
260 e := vals.Get("expires_in")
261 expires, _ := strconv.Atoi(e)
262 if expires != 0 {
263 token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
264 }
265 default:
266 var tj tokenJSON
267 if err = json.Unmarshal(body, &tj); err != nil {
268 return nil, err
269 }
270 token = &Token{
271 AccessToken: tj.AccessToken,
272 TokenType: tj.TokenType,
273 RefreshToken: tj.RefreshToken,
274 Expiry: tj.expiry(),
275 Raw: make(map[string]interface{}),
276 }
277 json.Unmarshal(body, &token.Raw) // no error checks for optional fields
278 }
279 if token.AccessToken == "" {
280 return nil, errors.New("oauth2: server response missing access_token")
281 }
282 return token, nil
283}
284
285type RetrieveError struct {
286 Response *http.Response
287 Body []byte
288}
289
290func (r *RetrieveError) Error() string {
291 return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
292}