You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

305 lines
9.0 KiB

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