token.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 contains support packages for oauth2 package.
  5. package internal
  6. import (
  7. "encoding/json"
  8. "fmt"
  9. "io"
  10. "io/ioutil"
  11. "mime"
  12. "net/http"
  13. "net/url"
  14. "strconv"
  15. "strings"
  16. "time"
  17. "golang.org/x/net/context"
  18. )
  19. // Token represents the crendentials used to authorize
  20. // the requests to access protected resources on the OAuth 2.0
  21. // provider's backend.
  22. //
  23. // This type is a mirror of oauth2.Token and exists to break
  24. // an otherwise-circular dependency. Other internal packages
  25. // should convert this Token into an oauth2.Token before use.
  26. type Token struct {
  27. // AccessToken is the token that authorizes and authenticates
  28. // the requests.
  29. AccessToken string
  30. // TokenType is the type of token.
  31. // The Type method returns either this or "Bearer", the default.
  32. TokenType string
  33. // RefreshToken is a token that's used by the application
  34. // (as opposed to the user) to refresh the access token
  35. // if it expires.
  36. RefreshToken string
  37. // Expiry is the optional expiration time of the access token.
  38. //
  39. // If zero, TokenSource implementations will reuse the same
  40. // token forever and RefreshToken or equivalent
  41. // mechanisms for that TokenSource will not be used.
  42. Expiry time.Time
  43. // Raw optionally contains extra metadata from the server
  44. // when updating a token.
  45. Raw interface{}
  46. }
  47. // tokenJSON is the struct representing the HTTP response from OAuth2
  48. // providers returning a token in JSON form.
  49. type tokenJSON struct {
  50. AccessToken string `json:"access_token"`
  51. TokenType string `json:"token_type"`
  52. RefreshToken string `json:"refresh_token"`
  53. ExpiresIn expirationTime `json:"expires_in"` // at least PayPal returns string, while most return number
  54. Expires expirationTime `json:"expires"` // broken Facebook spelling of expires_in
  55. }
  56. func (e *tokenJSON) expiry() (t time.Time) {
  57. if v := e.ExpiresIn; v != 0 {
  58. return time.Now().Add(time.Duration(v) * time.Second)
  59. }
  60. if v := e.Expires; v != 0 {
  61. return time.Now().Add(time.Duration(v) * time.Second)
  62. }
  63. return
  64. }
  65. type expirationTime int32
  66. func (e *expirationTime) UnmarshalJSON(b []byte) error {
  67. var n json.Number
  68. err := json.Unmarshal(b, &n)
  69. if err != nil {
  70. return err
  71. }
  72. i, err := n.Int64()
  73. if err != nil {
  74. return err
  75. }
  76. *e = expirationTime(i)
  77. return nil
  78. }
  79. var brokenAuthHeaderProviders = []string{
  80. "https://accounts.google.com/",
  81. "https://api.codeswholesale.com/oauth/token",
  82. "https://api.dropbox.com/",
  83. "https://api.dropboxapi.com/",
  84. "https://api.instagram.com/",
  85. "https://api.netatmo.net/",
  86. "https://api.odnoklassniki.ru/",
  87. "https://api.pushbullet.com/",
  88. "https://api.soundcloud.com/",
  89. "https://api.twitch.tv/",
  90. "https://app.box.com/",
  91. "https://connect.stripe.com/",
  92. "https://graph.facebook.com", // see https://github.com/golang/oauth2/issues/214
  93. "https://login.microsoft.net",
  94. "https://login.microsoftonline.com/",
  95. "https://login.salesforce.com/",
  96. "https://oauth.sandbox.trainingpeaks.com/",
  97. "https://oauth.trainingpeaks.com/",
  98. "https://oauth.vk.com/",
  99. "https://openapi.baidu.com/",
  100. "https://slack.com/",
  101. "https://test-sandbox.auth.corp.google.com",
  102. "https://test.salesforce.com/",
  103. "https://user.gini.net/",
  104. "https://www.douban.com/",
  105. "https://www.googleapis.com/",
  106. "https://www.linkedin.com/",
  107. "https://www.strava.com/oauth/",
  108. "https://www.wunderlist.com/oauth/",
  109. "https://api.patreon.com/",
  110. "https://sandbox.codeswholesale.com/oauth/token",
  111. "https://api.sipgate.com/v1/authorization/oauth",
  112. }
  113. // brokenAuthHeaderDomains lists broken providers that issue dynamic endpoints.
  114. var brokenAuthHeaderDomains = []string{
  115. ".force.com",
  116. ".myshopify.com",
  117. ".okta.com",
  118. ".oktapreview.com",
  119. }
  120. func RegisterBrokenAuthHeaderProvider(tokenURL string) {
  121. brokenAuthHeaderProviders = append(brokenAuthHeaderProviders, tokenURL)
  122. }
  123. // providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
  124. // implements the OAuth2 spec correctly
  125. // See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
  126. // In summary:
  127. // - Reddit only accepts client secret in the Authorization header
  128. // - Dropbox accepts either it in URL param or Auth header, but not both.
  129. // - Google only accepts URL param (not spec compliant?), not Auth header
  130. // - Stripe only accepts client secret in Auth header with Bearer method, not Basic
  131. func providerAuthHeaderWorks(tokenURL string) bool {
  132. for _, s := range brokenAuthHeaderProviders {
  133. if strings.HasPrefix(tokenURL, s) {
  134. // Some sites fail to implement the OAuth2 spec fully.
  135. return false
  136. }
  137. }
  138. if u, err := url.Parse(tokenURL); err == nil {
  139. for _, s := range brokenAuthHeaderDomains {
  140. if strings.HasSuffix(u.Host, s) {
  141. return false
  142. }
  143. }
  144. }
  145. // Assume the provider implements the spec properly
  146. // otherwise. We can add more exceptions as they're
  147. // discovered. We will _not_ be adding configurable hooks
  148. // to this package to let users select server bugs.
  149. return true
  150. }
  151. func RetrieveToken(ctx context.Context, clientID, clientSecret, tokenURL string, v url.Values) (*Token, error) {
  152. hc, err := ContextClient(ctx)
  153. if err != nil {
  154. return nil, err
  155. }
  156. bustedAuth := !providerAuthHeaderWorks(tokenURL)
  157. if bustedAuth {
  158. if clientID != "" {
  159. v.Set("client_id", clientID)
  160. }
  161. if clientSecret != "" {
  162. v.Set("client_secret", clientSecret)
  163. }
  164. }
  165. req, err := http.NewRequest("POST", tokenURL, strings.NewReader(v.Encode()))
  166. if err != nil {
  167. return nil, err
  168. }
  169. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  170. if !bustedAuth {
  171. req.SetBasicAuth(clientID, clientSecret)
  172. }
  173. r, err := hc.Do(req)
  174. if err != nil {
  175. return nil, err
  176. }
  177. defer r.Body.Close()
  178. body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
  179. if err != nil {
  180. return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
  181. }
  182. if code := r.StatusCode; code < 200 || code > 299 {
  183. return nil, fmt.Errorf("oauth2: cannot fetch token: %v\nResponse: %s", r.Status, body)
  184. }
  185. var token *Token
  186. content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
  187. switch content {
  188. case "application/x-www-form-urlencoded", "text/plain":
  189. vals, err := url.ParseQuery(string(body))
  190. if err != nil {
  191. return nil, err
  192. }
  193. token = &Token{
  194. AccessToken: vals.Get("access_token"),
  195. TokenType: vals.Get("token_type"),
  196. RefreshToken: vals.Get("refresh_token"),
  197. Raw: vals,
  198. }
  199. e := vals.Get("expires_in")
  200. if e == "" {
  201. // TODO(jbd): Facebook's OAuth2 implementation is broken and
  202. // returns expires_in field in expires. Remove the fallback to expires,
  203. // when Facebook fixes their implementation.
  204. e = vals.Get("expires")
  205. }
  206. expires, _ := strconv.Atoi(e)
  207. if expires != 0 {
  208. token.Expiry = time.Now().Add(time.Duration(expires) * time.Second)
  209. }
  210. default:
  211. var tj tokenJSON
  212. if err = json.Unmarshal(body, &tj); err != nil {
  213. return nil, err
  214. }
  215. token = &Token{
  216. AccessToken: tj.AccessToken,
  217. TokenType: tj.TokenType,
  218. RefreshToken: tj.RefreshToken,
  219. Expiry: tj.expiry(),
  220. Raw: make(map[string]interface{}),
  221. }
  222. json.Unmarshal(body, &token.Raw) // no error checks for optional fields
  223. }
  224. // Don't overwrite `RefreshToken` with an empty value
  225. // if this was a token refreshing request.
  226. if token.RefreshToken == "" {
  227. token.RefreshToken = v.Get("refresh_token")
  228. }
  229. return token, nil
  230. }