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.

oauth2.go 5.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // Copyright 2014 Google Inc. All Rights Reserved.
  2. // Copyright 2014 The Gogs Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. // Package oauth2 contains Martini handlers to provide
  6. // user login via an OAuth 2.0 backend.
  7. package oauth2
  8. import (
  9. "encoding/json"
  10. "net/http"
  11. "net/url"
  12. "strings"
  13. "time"
  14. "code.google.com/p/goauth2/oauth"
  15. "github.com/go-martini/martini"
  16. "github.com/gogits/session"
  17. "github.com/gogits/gogs/modules/log"
  18. "github.com/gogits/gogs/modules/middleware"
  19. )
  20. const (
  21. keyToken = "oauth2_token"
  22. keyNextPage = "next"
  23. )
  24. var (
  25. // Path to handle OAuth 2.0 logins.
  26. PathLogin = "/login"
  27. // Path to handle OAuth 2.0 logouts.
  28. PathLogout = "/logout"
  29. // Path to handle callback from OAuth 2.0 backend
  30. // to exchange credentials.
  31. PathCallback = "/oauth2callback"
  32. // Path to handle error cases.
  33. PathError = "/oauth2error"
  34. )
  35. // Represents OAuth2 backend options.
  36. type Options struct {
  37. ClientId string
  38. ClientSecret string
  39. RedirectURL string
  40. Scopes []string
  41. AuthUrl string
  42. TokenUrl string
  43. }
  44. // Represents a container that contains
  45. // user's OAuth 2.0 access and refresh tokens.
  46. type Tokens interface {
  47. Access() string
  48. Refresh() string
  49. IsExpired() bool
  50. ExpiryTime() time.Time
  51. ExtraData() map[string]string
  52. }
  53. type token struct {
  54. oauth.Token
  55. }
  56. func (t *token) ExtraData() map[string]string {
  57. return t.Extra
  58. }
  59. // Returns the access token.
  60. func (t *token) Access() string {
  61. return t.AccessToken
  62. }
  63. // Returns the refresh token.
  64. func (t *token) Refresh() string {
  65. return t.RefreshToken
  66. }
  67. // Returns whether the access token is
  68. // expired or not.
  69. func (t *token) IsExpired() bool {
  70. if t == nil {
  71. return true
  72. }
  73. return t.Expired()
  74. }
  75. // Returns the expiry time of the user's
  76. // access token.
  77. func (t *token) ExpiryTime() time.Time {
  78. return t.Expiry
  79. }
  80. // Returns a new Google OAuth 2.0 backend endpoint.
  81. func Google(opts *Options) martini.Handler {
  82. opts.AuthUrl = "https://accounts.google.com/o/oauth2/auth"
  83. opts.TokenUrl = "https://accounts.google.com/o/oauth2/token"
  84. return NewOAuth2Provider(opts)
  85. }
  86. // Returns a new Github OAuth 2.0 backend endpoint.
  87. func Github(opts *Options) martini.Handler {
  88. opts.AuthUrl = "https://github.com/login/oauth/authorize"
  89. opts.TokenUrl = "https://github.com/login/oauth/access_token"
  90. return NewOAuth2Provider(opts)
  91. }
  92. func Facebook(opts *Options) martini.Handler {
  93. opts.AuthUrl = "https://www.facebook.com/dialog/oauth"
  94. opts.TokenUrl = "https://graph.facebook.com/oauth/access_token"
  95. return NewOAuth2Provider(opts)
  96. }
  97. // Returns a generic OAuth 2.0 backend endpoint.
  98. func NewOAuth2Provider(opts *Options) martini.Handler {
  99. config := &oauth.Config{
  100. ClientId: opts.ClientId,
  101. ClientSecret: opts.ClientSecret,
  102. RedirectURL: opts.RedirectURL,
  103. Scope: strings.Join(opts.Scopes, " "),
  104. AuthURL: opts.AuthUrl,
  105. TokenURL: opts.TokenUrl,
  106. }
  107. transport := &oauth.Transport{
  108. Config: config,
  109. Transport: http.DefaultTransport,
  110. }
  111. return func(c martini.Context, ctx *middleware.Context) {
  112. if ctx.Req.Method == "GET" {
  113. switch ctx.Req.URL.Path {
  114. case PathLogin:
  115. login(transport, ctx)
  116. case PathLogout:
  117. logout(transport, ctx)
  118. case PathCallback:
  119. handleOAuth2Callback(transport, ctx)
  120. }
  121. }
  122. tk := unmarshallToken(ctx.Session)
  123. if tk != nil {
  124. // check if the access token is expired
  125. if tk.IsExpired() && tk.Refresh() == "" {
  126. ctx.Session.Delete(keyToken)
  127. tk = nil
  128. }
  129. }
  130. // Inject tokens.
  131. c.MapTo(tk, (*Tokens)(nil))
  132. }
  133. }
  134. // Handler that redirects user to the login page
  135. // if user is not logged in.
  136. // Sample usage:
  137. // m.Get("/login-required", oauth2.LoginRequired, func() ... {})
  138. var LoginRequired martini.Handler = func() martini.Handler {
  139. return func(c martini.Context, ctx *middleware.Context) {
  140. token := unmarshallToken(ctx.Session)
  141. if token == nil || token.IsExpired() {
  142. next := url.QueryEscape(ctx.Req.URL.RequestURI())
  143. ctx.Redirect(PathLogin + "?next=" + next)
  144. return
  145. }
  146. }
  147. }()
  148. func login(t *oauth.Transport, ctx *middleware.Context) {
  149. next := extractPath(ctx.Query(keyNextPage))
  150. if ctx.Session.Get(keyToken) == nil {
  151. // User is not logged in.
  152. ctx.Redirect(t.Config.AuthCodeURL(next))
  153. return
  154. }
  155. // No need to login, redirect to the next page.
  156. ctx.Redirect(next)
  157. }
  158. func logout(t *oauth.Transport, ctx *middleware.Context) {
  159. next := extractPath(ctx.Query(keyNextPage))
  160. ctx.Session.Delete(keyToken)
  161. ctx.Redirect(next)
  162. }
  163. func handleOAuth2Callback(t *oauth.Transport, ctx *middleware.Context) {
  164. if errMsg := ctx.Query("error_description"); len(errMsg) > 0 {
  165. log.Error("oauth2.handleOAuth2Callback: %s", errMsg)
  166. return
  167. }
  168. next := extractPath(ctx.Query("state"))
  169. code := ctx.Query("code")
  170. tk, err := t.Exchange(code)
  171. if err != nil {
  172. // Pass the error message, or allow dev to provide its own
  173. // error handler.
  174. log.Error("oauth2.handleOAuth2Callback(token.Exchange): %v", err)
  175. // ctx.Redirect(PathError)
  176. return
  177. }
  178. // Store the credentials in the session.
  179. val, _ := json.Marshal(tk)
  180. ctx.Session.Set(keyToken, val)
  181. ctx.Redirect(next)
  182. }
  183. func unmarshallToken(s session.SessionStore) (t *token) {
  184. if s.Get(keyToken) == nil {
  185. return
  186. }
  187. data := s.Get(keyToken).([]byte)
  188. var tk oauth.Token
  189. json.Unmarshal(data, &tk)
  190. return &token{tk}
  191. }
  192. func extractPath(next string) string {
  193. n, err := url.Parse(next)
  194. if err != nil {
  195. return "/"
  196. }
  197. return n.Path
  198. }