token.go 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  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. package internal
  5. import (
  6. "context"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. "math"
  13. "mime"
  14. "net/http"
  15. "net/url"
  16. "strconv"
  17. "strings"
  18. "sync"
  19. "time"
  20. )
  21. // Token represents the credentials used to authorize
  22. // the requests to access protected resources on the OAuth 2.0
  23. // provider's backend.
  24. //
  25. // This type is a mirror of oauth2.Token and exists to break
  26. // an otherwise-circular dependency. Other internal packages
  27. // should convert this Token into an oauth2.Token before use.
  28. type Token struct {
  29. // AccessToken is the token that authorizes and authenticates
  30. // the requests.
  31. AccessToken string
  32. // TokenType is the type of token.
  33. // The Type method returns either this or "Bearer", the default.
  34. TokenType string
  35. // RefreshToken is a token that's used by the application
  36. // (as opposed to the user) to refresh the access token
  37. // if it expires.
  38. RefreshToken string
  39. // Expiry is the optional expiration time of the access token.
  40. //
  41. // If zero, TokenSource implementations will reuse the same
  42. // token forever and RefreshToken or equivalent
  43. // mechanisms for that TokenSource will not be used.
  44. Expiry time.Time
  45. // Raw optionally contains extra metadata from the server
  46. // when updating a token.
  47. Raw interface{}
  48. }
  49. // tokenJSON is the struct representing the HTTP response from OAuth2
  50. // providers returning a token or error in JSON form.
  51. // https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
  52. type tokenJSON struct {
  53. AccessToken string `json:"access_token"`
  54. TokenType string `json:"token_type"`
  55. RefreshToken string `json:"refresh_token"`
  56. ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
  57. // error fields
  58. // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
  59. ErrorCode string `json:"error"`
  60. ErrorDescription string `json:"error_description"`
  61. ErrorURI string `json:"error_uri"`
  62. }
  63. func (e *tokenJSON) expiry() (t time.Time) {
  64. if v := e.ExpiresIn; v != 0 {
  65. return time.Now().Add(time.Duration(v) * time.Second)
  66. }
  67. return
  68. }
  69. type expirationTime int32
  70. func (e *expirationTime) UnmarshalJSON(b []byte) error {
  71. if len(b) == 0 || string(b) == "null" {
  72. return nil
  73. }
  74. var n json.Number
  75. err := json.Unmarshal(b, &n)
  76. if err != nil {
  77. return err
  78. }
  79. i, err := n.Int64()
  80. if err != nil {
  81. return err
  82. }
  83. if i > math.MaxInt32 {
  84. i = math.MaxInt32
  85. }
  86. *e = expirationTime(i)
  87. return nil
  88. }
  89. // RegisterBrokenAuthHeaderProvider previously did something. It is now a no-op.
  90. //
  91. // Deprecated: this function no longer does anything. Caller code that
  92. // wants to avoid potential extra HTTP requests made during
  93. // auto-probing of the provider's auth style should set
  94. // Endpoint.AuthStyle.
  95. func RegisterBrokenAuthHeaderProvider(tokenURL string) {}
  96. // AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.
  97. type AuthStyle int
  98. const (
  99. AuthStyleUnknown AuthStyle = 0
  100. AuthStyleInParams AuthStyle = 1
  101. AuthStyleInHeader AuthStyle = 2
  102. )
  103. // authStyleCache is the set of tokenURLs we've successfully used via
  104. // RetrieveToken and which style auth we ended up using.
  105. // It's called a cache, but it doesn't (yet?) shrink. It's expected that
  106. // the set of OAuth2 servers a program contacts over time is fixed and
  107. // small.
  108. var authStyleCache struct {
  109. sync.Mutex
  110. m map[string]AuthStyle // keyed by tokenURL
  111. }
  112. // ResetAuthCache resets the global authentication style cache used
  113. // for AuthStyleUnknown token requests.
  114. func ResetAuthCache() {
  115. authStyleCache.Lock()
  116. defer authStyleCache.Unlock()
  117. authStyleCache.m = nil
  118. }
  119. // lookupAuthStyle reports which auth style we last used with tokenURL
  120. // when calling RetrieveToken and whether we have ever done so.
  121. func lookupAuthStyle(tokenURL string) (style AuthStyle, ok bool) {
  122. authStyleCache.Lock()
  123. defer authStyleCache.Unlock()
  124. style, ok = authStyleCache.m[tokenURL]
  125. return
  126. }
  127. // setAuthStyle adds an entry to authStyleCache, documented above.
  128. func setAuthStyle(tokenURL string, v AuthStyle) {
  129. authStyleCache.Lock()
  130. defer authStyleCache.Unlock()
  131. if authStyleCache.m == nil {
  132. authStyleCache.m = make(map[string]AuthStyle)
  133. }
  134. authStyleCache.m[tokenURL] = v
  135. }
  136. // newTokenRequest returns a new *http.Request to retrieve a new token
  137. // from tokenURL using the provided clientID, clientSecret, and POST
  138. // body parameters.
  139. //
  140. // inParams is whether the clientID & clientSecret should be encoded
  141. // as the POST body. An 'inParams' value of true means to send it in
  142. // the POST body (along with any values in v); false means to send it
  143. // in the Authorization header.
  144. func newTokenRequest(tokenURL, clientID, clientSecret string, v url.Values, authStyle AuthStyle) (*http.Request, error) {
  145. if authStyle == AuthStyleInParams {
  146. v = cloneURLValues(v)
  147. if clientID != "" {
  148. v.Set("client_id", clientID)
  149. }
  150. if clientSecret != "" {
  151. v.Set("client_secret", clientSecret)
  152. }
  153. }
  154. req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
  155. if err != nil {
  156. return nil, err
  157. }
  158. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  159. if authStyle == AuthStyleInHeader {
  160. req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret))
  161. }
  162. return req, nil
  163. }
  164. func cloneURLValues(v url.Values) url.Values {
  165. v2 := make(url.Values, len(v))
  166. for k, vv := range v {
  167. v2[k] = append([]string(nil), vv...)
  168. }
  169. return v2
  170. }
  171. func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle) (*Token, error) {
  172. needsAuthStyleProbe := authStyle == 0
  173. if needsAuthStyleProbe {
  174. if style, ok := lookupAuthStyle(tokenURL); ok {
  175. authStyle = style
  176. needsAuthStyleProbe = false
  177. } else {
  178. authStyle = AuthStyleInHeader // the first way we'll try
  179. }
  180. }
  181. req, err := newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
  182. if err != nil {
  183. return nil, err
  184. }
  185. token, err := doTokenRoundTrip(ctx, req)
  186. if err != nil && needsAuthStyleProbe {
  187. // If we get an error, assume the server wants the
  188. // clientID & clientSecret in a different form.
  189. // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
  190. // In summary:
  191. // - Reddit only accepts client secret in the Authorization header
  192. // - Dropbox accepts either it in URL param or Auth header, but not both.
  193. // - Google only accepts URL param (not spec compliant?), not Auth header
  194. // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
  195. //
  196. // We used to maintain a big table in this code of all the sites and which way
  197. // they went, but maintaining it didn't scale & got annoying.
  198. // So just try both ways.
  199. authStyle = AuthStyleInParams // the second way we'll try
  200. req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
  201. token, err = doTokenRoundTrip(ctx, req)
  202. }
  203. if needsAuthStyleProbe && err == nil {
  204. setAuthStyle(tokenURL, authStyle)
  205. }
  206. // Don't overwrite `RefreshToken` with an empty value
  207. // if this was a token refreshing request.
  208. if token != nil && token.RefreshToken == "" {
  209. token.RefreshToken = v.Get("refresh_token")
  210. }
  211. return token, err
  212. }
  213. func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
  214. r, err := ContextClient(ctx).Do(req.WithContext(ctx))
  215. if err != nil {
  216. return nil, err
  217. }
  218. body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
  219. r.Body.Close()
  220. if err != nil {
  221. return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
  222. }
  223. failureStatus := r.StatusCode < 200 || r.StatusCode > 299
  224. retrieveError := &RetrieveError{
  225. Response: r,
  226. Body: body,
  227. // attempt to populate error detail below
  228. }
  229. var token *Token
  230. content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
  231. switch content {
  232. case "application/x-www-form-urlencoded", "text/plain":
  233. // some endpoints return a query string
  234. vals, err := url.ParseQuery(string(body))
  235. if err != nil {
  236. if failureStatus {
  237. return nil, retrieveError
  238. }
  239. return nil, fmt.Errorf("oauth2: cannot parse response: %v", err)
  240. }
  241. retrieveError.ErrorCode = vals.Get("error")
  242. retrieveError.ErrorDescription = vals.Get("error_description")
  243. retrieveError.ErrorURI = vals.Get("error_uri")
  244. token = &Token{
  245. AccessToken: vals.Get("access_token"),
  246. TokenType: vals.Get("token_type"),
  247. RefreshToken: vals.Get("refresh_token"),
  248. Raw: vals,
  249. }
  250. e := vals.Get("expires_in")
  251. expires, _ := strconv.Atoi(e)
  252. if expires != 0 {
  253. token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
  254. }
  255. default:
  256. var tj tokenJSON
  257. if err = json.Unmarshal(body, &tj); err != nil {
  258. if failureStatus {
  259. return nil, retrieveError
  260. }
  261. return nil, fmt.Errorf("oauth2: cannot parse json: %v", err)
  262. }
  263. retrieveError.ErrorCode = tj.ErrorCode
  264. retrieveError.ErrorDescription = tj.ErrorDescription
  265. retrieveError.ErrorURI = tj.ErrorURI
  266. token = &Token{
  267. AccessToken: tj.AccessToken,
  268. TokenType: tj.TokenType,
  269. RefreshToken: tj.RefreshToken,
  270. Expiry: tj.expiry(),
  271. Raw: make(map[string]interface{}),
  272. }
  273. json.Unmarshal(body, &token.Raw) // no error checks for optional fields
  274. }
  275. // according to spec, servers should respond status 400 in error case
  276. // https://www.rfc-editor.org/rfc/rfc6749#section-5.2
  277. // but some unorthodox servers respond 200 in error case
  278. if failureStatus || retrieveError.ErrorCode != "" {
  279. return nil, retrieveError
  280. }
  281. if token.AccessToken == "" {
  282. return nil, errors.New("oauth2: server response missing access_token")
  283. }
  284. return token, nil
  285. }
  286. // mirrors oauth2.RetrieveError
  287. type RetrieveError struct {
  288. Response *http.Response
  289. Body []byte
  290. ErrorCode string
  291. ErrorDescription string
  292. ErrorURI string
  293. }
  294. func (r *RetrieveError) Error() string {
  295. if r.ErrorCode != "" {
  296. s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
  297. if r.ErrorDescription != "" {
  298. s += fmt.Sprintf(" %q", r.ErrorDescription)
  299. }
  300. if r.ErrorURI != "" {
  301. s += fmt.Sprintf(" %q", r.ErrorURI)
  302. }
  303. return s
  304. }
  305. return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
  306. }