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