token.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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. "math"
  12. "mime"
  13. "net/http"
  14. "net/url"
  15. "strconv"
  16. "strings"
  17. "sync"
  18. "sync/atomic"
  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 [golang.org/x/oauth2.Token] and exists to break
  26. // an otherwise-circular dependency. Other internal packages
  27. // should convert this Token into an [golang.org/x/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. // ExpiresIn is the OAuth2 wire format "expires_in" field,
  46. // which specifies how many seconds later the token expires,
  47. // relative to an unknown time base approximately around "now".
  48. // It is the application's responsibility to populate
  49. // `Expiry` from `ExpiresIn` when required.
  50. ExpiresIn int64 `json:"expires_in,omitempty"`
  51. // Raw optionally contains extra metadata from the server
  52. // when updating a token.
  53. Raw any
  54. }
  55. // tokenJSON is the struct representing the HTTP response from OAuth2
  56. // providers returning a token or error in JSON form.
  57. // https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
  58. type tokenJSON struct {
  59. AccessToken string `json:"access_token"`
  60. TokenType string `json:"token_type"`
  61. RefreshToken string `json:"refresh_token"`
  62. ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
  63. // error fields
  64. // https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
  65. ErrorCode string `json:"error"`
  66. ErrorDescription string `json:"error_description"`
  67. ErrorURI string `json:"error_uri"`
  68. }
  69. func (e *tokenJSON) expiry() (t time.Time) {
  70. if v := e.ExpiresIn; v != 0 {
  71. return time.Now().Add(time.Duration(v) * time.Second)
  72. }
  73. return
  74. }
  75. type expirationTime int32
  76. func (e *expirationTime) UnmarshalJSON(b []byte) error {
  77. if len(b) == 0 || string(b) == "null" {
  78. return nil
  79. }
  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. if i > math.MaxInt32 {
  90. i = math.MaxInt32
  91. }
  92. *e = expirationTime(i)
  93. return nil
  94. }
  95. // AuthStyle is a copy of the golang.org/x/oauth2 package's AuthStyle type.
  96. type AuthStyle int
  97. const (
  98. AuthStyleUnknown AuthStyle = 0
  99. AuthStyleInParams AuthStyle = 1
  100. AuthStyleInHeader AuthStyle = 2
  101. )
  102. // LazyAuthStyleCache is a backwards compatibility compromise to let Configs
  103. // have a lazily-initialized AuthStyleCache.
  104. //
  105. // The two users of this, oauth2.Config and oauth2/clientcredentials.Config,
  106. // both would ideally just embed an unexported AuthStyleCache but because both
  107. // were historically allowed to be copied by value we can't retroactively add an
  108. // uncopyable Mutex to them.
  109. //
  110. // We could use an atomic.Pointer, but that was added recently enough (in Go
  111. // 1.18) that we'd break Go 1.17 users where the tests as of 2023-08-03
  112. // still pass. By using an atomic.Value, it supports both Go 1.17 and
  113. // copying by value, even if that's not ideal.
  114. type LazyAuthStyleCache struct {
  115. v atomic.Value // of *AuthStyleCache
  116. }
  117. func (lc *LazyAuthStyleCache) Get() *AuthStyleCache {
  118. if c, ok := lc.v.Load().(*AuthStyleCache); ok {
  119. return c
  120. }
  121. c := new(AuthStyleCache)
  122. if !lc.v.CompareAndSwap(nil, c) {
  123. c = lc.v.Load().(*AuthStyleCache)
  124. }
  125. return c
  126. }
  127. type authStyleCacheKey struct {
  128. url string
  129. clientID string
  130. }
  131. // AuthStyleCache is the set of tokenURLs we've successfully used via
  132. // RetrieveToken and which style auth we ended up using.
  133. // It's called a cache, but it doesn't (yet?) shrink. It's expected that
  134. // the set of OAuth2 servers a program contacts over time is fixed and
  135. // small.
  136. type AuthStyleCache struct {
  137. mu sync.Mutex
  138. m map[authStyleCacheKey]AuthStyle
  139. }
  140. // lookupAuthStyle reports which auth style we last used with tokenURL
  141. // when calling RetrieveToken and whether we have ever done so.
  142. func (c *AuthStyleCache) lookupAuthStyle(tokenURL, clientID string) (style AuthStyle, ok bool) {
  143. c.mu.Lock()
  144. defer c.mu.Unlock()
  145. style, ok = c.m[authStyleCacheKey{tokenURL, clientID}]
  146. return
  147. }
  148. // setAuthStyle adds an entry to authStyleCache, documented above.
  149. func (c *AuthStyleCache) setAuthStyle(tokenURL, clientID string, v AuthStyle) {
  150. c.mu.Lock()
  151. defer c.mu.Unlock()
  152. if c.m == nil {
  153. c.m = make(map[authStyleCacheKey]AuthStyle)
  154. }
  155. c.m[authStyleCacheKey{tokenURL, clientID}] = v
  156. }
  157. // newTokenRequest returns a new *http.Request to retrieve a new token
  158. // from tokenURL using the provided clientID, clientSecret, and POST
  159. // body parameters.
  160. //
  161. // inParams is whether the clientID & clientSecret should be encoded
  162. // as the POST body. An 'inParams' value of true means to send it in
  163. // the POST body (along with any values in v); false means to send it
  164. // in the Authorization header.
  165. func newTokenRequest(tokenURL, clientID, clientSecret string, v url.Values, authStyle AuthStyle) (*http.Request, error) {
  166. if authStyle == AuthStyleInParams {
  167. v = cloneURLValues(v)
  168. if clientID != "" {
  169. v.Set("client_id", clientID)
  170. }
  171. if clientSecret != "" {
  172. v.Set("client_secret", clientSecret)
  173. }
  174. }
  175. req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
  176. if err != nil {
  177. return nil, err
  178. }
  179. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  180. if authStyle == AuthStyleInHeader {
  181. req.SetBasicAuth(url.QueryEscape(clientID), url.QueryEscape(clientSecret))
  182. }
  183. return req, nil
  184. }
  185. func cloneURLValues(v url.Values) url.Values {
  186. v2 := make(url.Values, len(v))
  187. for k, vv := range v {
  188. v2[k] = append([]string(nil), vv...)
  189. }
  190. return v2
  191. }
  192. func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values, authStyle AuthStyle, styleCache *AuthStyleCache) (*Token, error) {
  193. needsAuthStyleProbe := authStyle == AuthStyleUnknown
  194. if needsAuthStyleProbe {
  195. if style, ok := styleCache.lookupAuthStyle(tokenURL, clientID); ok {
  196. authStyle = style
  197. needsAuthStyleProbe = false
  198. } else {
  199. authStyle = AuthStyleInHeader // the first way we'll try
  200. }
  201. }
  202. req, err := newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
  203. if err != nil {
  204. return nil, err
  205. }
  206. token, err := doTokenRoundTrip(ctx, req)
  207. if err != nil && needsAuthStyleProbe {
  208. // If we get an error, assume the server wants the
  209. // clientID & clientSecret in a different form.
  210. // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
  211. // In summary:
  212. // - Reddit only accepts client secret in the Authorization header
  213. // - Dropbox accepts either it in URL param or Auth header, but not both.
  214. // - Google only accepts URL param (not spec compliant?), not Auth header
  215. // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
  216. //
  217. // We used to maintain a big table in this code of all the sites and which way
  218. // they went, but maintaining it didn't scale & got annoying.
  219. // So just try both ways.
  220. authStyle = AuthStyleInParams // the second way we'll try
  221. req, _ = newTokenRequest(tokenURL, clientID, clientSecret, v, authStyle)
  222. token, err = doTokenRoundTrip(ctx, req)
  223. }
  224. if needsAuthStyleProbe && err == nil {
  225. styleCache.setAuthStyle(tokenURL, clientID, authStyle)
  226. }
  227. // Don't overwrite `RefreshToken` with an empty value
  228. // if this was a token refreshing request.
  229. if token != nil && token.RefreshToken == "" {
  230. token.RefreshToken = v.Get("refresh_token")
  231. }
  232. return token, err
  233. }
  234. func doTokenRoundTrip(ctx context.Context, req *http.Request) (*Token, error) {
  235. r, err := ContextClient(ctx).Do(req.WithContext(ctx))
  236. if err != nil {
  237. return nil, err
  238. }
  239. body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
  240. r.Body.Close()
  241. if err != nil {
  242. return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
  243. }
  244. failureStatus := r.StatusCode < 200 || r.StatusCode > 299
  245. retrieveError := &RetrieveError{
  246. Response: r,
  247. Body: body,
  248. // attempt to populate error detail below
  249. }
  250. var token *Token
  251. content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
  252. switch content {
  253. case "application/x-www-form-urlencoded", "text/plain":
  254. // some endpoints return a query string
  255. vals, err := url.ParseQuery(string(body))
  256. if err != nil {
  257. if failureStatus {
  258. return nil, retrieveError
  259. }
  260. return nil, fmt.Errorf("oauth2: cannot parse response: %v", err)
  261. }
  262. retrieveError.ErrorCode = vals.Get("error")
  263. retrieveError.ErrorDescription = vals.Get("error_description")
  264. retrieveError.ErrorURI = vals.Get("error_uri")
  265. token = &Token{
  266. AccessToken: vals.Get("access_token"),
  267. TokenType: vals.Get("token_type"),
  268. RefreshToken: vals.Get("refresh_token"),
  269. Raw: vals,
  270. }
  271. e := vals.Get("expires_in")
  272. expires, _ := strconv.Atoi(e)
  273. if expires != 0 {
  274. token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
  275. }
  276. default:
  277. var tj tokenJSON
  278. if err = json.Unmarshal(body, &tj); err != nil {
  279. if failureStatus {
  280. return nil, retrieveError
  281. }
  282. return nil, fmt.Errorf("oauth2: cannot parse json: %v", err)
  283. }
  284. retrieveError.ErrorCode = tj.ErrorCode
  285. retrieveError.ErrorDescription = tj.ErrorDescription
  286. retrieveError.ErrorURI = tj.ErrorURI
  287. token = &Token{
  288. AccessToken: tj.AccessToken,
  289. TokenType: tj.TokenType,
  290. RefreshToken: tj.RefreshToken,
  291. Expiry: tj.expiry(),
  292. ExpiresIn: int64(tj.ExpiresIn),
  293. Raw: make(map[string]any),
  294. }
  295. json.Unmarshal(body, &token.Raw) // no error checks for optional fields
  296. }
  297. // according to spec, servers should respond status 400 in error case
  298. // https://www.rfc-editor.org/rfc/rfc6749#section-5.2
  299. // but some unorthodox servers respond 200 in error case
  300. if failureStatus || retrieveError.ErrorCode != "" {
  301. return nil, retrieveError
  302. }
  303. if token.AccessToken == "" {
  304. return nil, errors.New("oauth2: server response missing access_token")
  305. }
  306. return token, nil
  307. }
  308. // mirrors oauth2.RetrieveError
  309. type RetrieveError struct {
  310. Response *http.Response
  311. Body []byte
  312. ErrorCode string
  313. ErrorDescription string
  314. ErrorURI string
  315. }
  316. func (r *RetrieveError) Error() string {
  317. if r.ErrorCode != "" {
  318. s := fmt.Sprintf("oauth2: %q", r.ErrorCode)
  319. if r.ErrorDescription != "" {
  320. s += fmt.Sprintf(" %q", r.ErrorDescription)
  321. }
  322. if r.ErrorURI != "" {
  323. s += fmt.Sprintf(" %q", r.ErrorURI)
  324. }
  325. return s
  326. }
  327. return fmt.Sprintf("oauth2: cannot fetch token: %v\nResponse: %s", r.Response.Status, r.Body)
  328. }