A clean, Markdown-based publishing platform made for writers. Write together, and build a community. https://writefreely.org
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 

350 righe
12 KiB

  1. package writefreely
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "io/ioutil"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "github.com/gorilla/mux"
  13. "github.com/gorilla/sessions"
  14. "github.com/writeas/impart"
  15. "github.com/writeas/web-core/log"
  16. "github.com/writeas/writefreely/config"
  17. )
  18. // TokenResponse contains data returned when a token is created either
  19. // through a code exchange or using a refresh token.
  20. type TokenResponse struct {
  21. AccessToken string `json:"access_token"`
  22. ExpiresIn int `json:"expires_in"`
  23. RefreshToken string `json:"refresh_token"`
  24. TokenType string `json:"token_type"`
  25. Error string `json:"error"`
  26. }
  27. // InspectResponse contains data returned when an access token is inspected.
  28. type InspectResponse struct {
  29. ClientID string `json:"client_id"`
  30. UserID string `json:"user_id"`
  31. ExpiresAt time.Time `json:"expires_at"`
  32. Username string `json:"username"`
  33. DisplayName string `json:"-"`
  34. Email string `json:"email"`
  35. Error string `json:"error"`
  36. }
  37. // tokenRequestMaxLen is the most bytes that we'll read from the /oauth/token
  38. // endpoint. One megabyte is plenty.
  39. const tokenRequestMaxLen = 1000000
  40. // infoRequestMaxLen is the most bytes that we'll read from the
  41. // /oauth/inspect endpoint.
  42. const infoRequestMaxLen = 1000000
  43. // OAuthDatastoreProvider provides a minimal interface of data store, config,
  44. // and session store for use with the oauth handlers.
  45. type OAuthDatastoreProvider interface {
  46. DB() OAuthDatastore
  47. Config() *config.Config
  48. SessionStore() sessions.Store
  49. }
  50. // OAuthDatastore provides a minimal interface of data store methods used in
  51. // oauth functionality.
  52. type OAuthDatastore interface {
  53. GetIDForRemoteUser(context.Context, string, string, string) (int64, error)
  54. RecordRemoteUserID(context.Context, int64, string, string, string, string) error
  55. ValidateOAuthState(context.Context, string) (string, string, int64, error)
  56. GenerateOAuthState(context.Context, string, string, int64) (string, error)
  57. CreateUser(*config.Config, *User, string) error
  58. GetUserByID(int64) (*User, error)
  59. }
  60. type HttpClient interface {
  61. Do(req *http.Request) (*http.Response, error)
  62. }
  63. type oauthClient interface {
  64. GetProvider() string
  65. GetClientID() string
  66. GetCallbackLocation() string
  67. buildLoginURL(state string) (string, error)
  68. exchangeOauthCode(ctx context.Context, code string) (*TokenResponse, error)
  69. inspectOauthAccessToken(ctx context.Context, accessToken string) (*InspectResponse, error)
  70. }
  71. type callbackProxyClient struct {
  72. server string
  73. callbackLocation string
  74. httpClient HttpClient
  75. }
  76. type oauthHandler struct {
  77. Config *config.Config
  78. DB OAuthDatastore
  79. Store sessions.Store
  80. EmailKey []byte
  81. oauthClient oauthClient
  82. callbackProxy *callbackProxyClient
  83. }
  84. func (h oauthHandler) viewOauthInit(app *App, w http.ResponseWriter, r *http.Request) error {
  85. ctx := r.Context()
  86. var attachUser int64
  87. if attach := r.URL.Query().Get("attach"); attach == "t" {
  88. user, _ := getUserAndSession(app, r)
  89. if user == nil {
  90. return impart.HTTPError{http.StatusInternalServerError, "cannot attach auth to user: user not found in session"}
  91. }
  92. attachUser = user.ID
  93. }
  94. state, err := h.DB.GenerateOAuthState(ctx, h.oauthClient.GetProvider(), h.oauthClient.GetClientID(), attachUser)
  95. if err != nil {
  96. log.Error("viewOauthInit error: %s", err)
  97. return impart.HTTPError{http.StatusInternalServerError, "could not prepare oauth redirect url"}
  98. }
  99. if h.callbackProxy != nil {
  100. if err := h.callbackProxy.register(ctx, state); err != nil {
  101. log.Error("viewOauthInit error: %s", err)
  102. return impart.HTTPError{http.StatusInternalServerError, "could not register state server"}
  103. }
  104. }
  105. location, err := h.oauthClient.buildLoginURL(state)
  106. if err != nil {
  107. log.Error("viewOauthInit error: %s", err)
  108. return impart.HTTPError{http.StatusInternalServerError, "could not prepare oauth redirect url"}
  109. }
  110. return impart.HTTPError{http.StatusTemporaryRedirect, location}
  111. }
  112. func configureSlackOauth(parentHandler *Handler, r *mux.Router, app *App) {
  113. if app.Config().SlackOauth.ClientID != "" {
  114. callbackLocation := app.Config().App.Host + "/oauth/callback/slack"
  115. var stateRegisterClient *callbackProxyClient = nil
  116. if app.Config().SlackOauth.CallbackProxyAPI != "" {
  117. stateRegisterClient = &callbackProxyClient{
  118. server: app.Config().SlackOauth.CallbackProxyAPI,
  119. callbackLocation: app.Config().App.Host + "/oauth/callback/slack",
  120. httpClient: config.DefaultHTTPClient(),
  121. }
  122. callbackLocation = app.Config().SlackOauth.CallbackProxy
  123. }
  124. oauthClient := slackOauthClient{
  125. ClientID: app.Config().SlackOauth.ClientID,
  126. ClientSecret: app.Config().SlackOauth.ClientSecret,
  127. TeamID: app.Config().SlackOauth.TeamID,
  128. HttpClient: config.DefaultHTTPClient(),
  129. CallbackLocation: callbackLocation,
  130. }
  131. configureOauthRoutes(parentHandler, r, app, oauthClient, stateRegisterClient)
  132. }
  133. }
  134. func configureWriteAsOauth(parentHandler *Handler, r *mux.Router, app *App) {
  135. if app.Config().WriteAsOauth.ClientID != "" {
  136. callbackLocation := app.Config().App.Host + "/oauth/callback/write.as"
  137. var callbackProxy *callbackProxyClient = nil
  138. if app.Config().WriteAsOauth.CallbackProxy != "" {
  139. callbackProxy = &callbackProxyClient{
  140. server: app.Config().WriteAsOauth.CallbackProxyAPI,
  141. callbackLocation: app.Config().App.Host + "/oauth/callback/write.as",
  142. httpClient: config.DefaultHTTPClient(),
  143. }
  144. callbackLocation = app.Config().WriteAsOauth.CallbackProxy
  145. }
  146. oauthClient := writeAsOauthClient{
  147. ClientID: app.Config().WriteAsOauth.ClientID,
  148. ClientSecret: app.Config().WriteAsOauth.ClientSecret,
  149. ExchangeLocation: config.OrDefaultString(app.Config().WriteAsOauth.TokenLocation, writeAsExchangeLocation),
  150. InspectLocation: config.OrDefaultString(app.Config().WriteAsOauth.InspectLocation, writeAsIdentityLocation),
  151. AuthLocation: config.OrDefaultString(app.Config().WriteAsOauth.AuthLocation, writeAsAuthLocation),
  152. HttpClient: config.DefaultHTTPClient(),
  153. CallbackLocation: callbackLocation,
  154. }
  155. configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
  156. }
  157. }
  158. func configureGitlabOauth(parentHandler *Handler, r *mux.Router, app *App) {
  159. if app.Config().GitlabOauth.ClientID != "" {
  160. callbackLocation := app.Config().App.Host + "/oauth/callback/gitlab"
  161. var callbackProxy *callbackProxyClient = nil
  162. if app.Config().GitlabOauth.CallbackProxy != "" {
  163. callbackProxy = &callbackProxyClient{
  164. server: app.Config().GitlabOauth.CallbackProxyAPI,
  165. callbackLocation: app.Config().App.Host + "/oauth/callback/gitlab",
  166. httpClient: config.DefaultHTTPClient(),
  167. }
  168. callbackLocation = app.Config().GitlabOauth.CallbackProxy
  169. }
  170. address := config.OrDefaultString(app.Config().GitlabOauth.Host, gitlabHost)
  171. oauthClient := gitlabOauthClient{
  172. ClientID: app.Config().GitlabOauth.ClientID,
  173. ClientSecret: app.Config().GitlabOauth.ClientSecret,
  174. ExchangeLocation: address + "/oauth/token",
  175. InspectLocation: address + "/api/v4/user",
  176. AuthLocation: address + "/oauth/authorize",
  177. HttpClient: config.DefaultHTTPClient(),
  178. CallbackLocation: callbackLocation,
  179. }
  180. configureOauthRoutes(parentHandler, r, app, oauthClient, callbackProxy)
  181. }
  182. }
  183. func configureOauthRoutes(parentHandler *Handler, r *mux.Router, app *App, oauthClient oauthClient, callbackProxy *callbackProxyClient) {
  184. handler := &oauthHandler{
  185. Config: app.Config(),
  186. DB: app.DB(),
  187. Store: app.SessionStore(),
  188. oauthClient: oauthClient,
  189. EmailKey: app.keys.EmailKey,
  190. callbackProxy: callbackProxy,
  191. }
  192. r.HandleFunc("/oauth/"+oauthClient.GetProvider(), parentHandler.OAuth(handler.viewOauthInit)).Methods("GET")
  193. r.HandleFunc("/oauth/callback/"+oauthClient.GetProvider(), parentHandler.OAuth(handler.viewOauthCallback)).Methods("GET")
  194. r.HandleFunc("/oauth/signup", parentHandler.OAuth(handler.viewOauthSignup)).Methods("POST")
  195. }
  196. func (h oauthHandler) viewOauthCallback(app *App, w http.ResponseWriter, r *http.Request) error {
  197. ctx := r.Context()
  198. code := r.FormValue("code")
  199. state := r.FormValue("state")
  200. provider, clientID, attachUserID, err := h.DB.ValidateOAuthState(ctx, state)
  201. if err != nil {
  202. log.Error("Unable to ValidateOAuthState: %s", err)
  203. return impart.HTTPError{http.StatusInternalServerError, err.Error()}
  204. }
  205. tokenResponse, err := h.oauthClient.exchangeOauthCode(ctx, code)
  206. if err != nil {
  207. log.Error("Unable to exchangeOauthCode: %s", err)
  208. return impart.HTTPError{http.StatusInternalServerError, err.Error()}
  209. }
  210. // Now that we have the access token, let's use it real quick to make sur
  211. // it really really works.
  212. tokenInfo, err := h.oauthClient.inspectOauthAccessToken(ctx, tokenResponse.AccessToken)
  213. if err != nil {
  214. log.Error("Unable to inspectOauthAccessToken: %s", err)
  215. return impart.HTTPError{http.StatusInternalServerError, err.Error()}
  216. }
  217. localUserID, err := h.DB.GetIDForRemoteUser(ctx, tokenInfo.UserID, provider, clientID)
  218. if err != nil {
  219. log.Error("Unable to GetIDForRemoteUser: %s", err)
  220. return impart.HTTPError{http.StatusInternalServerError, err.Error()}
  221. }
  222. if localUserID != -1 && attachUserID > 0 {
  223. if err = addSessionFlash(app, w, r, "This Slack account is already attached to another user.", nil); err != nil {
  224. return impart.HTTPError{Status: http.StatusInternalServerError, Message: err.Error()}
  225. }
  226. return impart.HTTPError{http.StatusFound, "/me/settings"}
  227. }
  228. if localUserID != -1 {
  229. user, err := h.DB.GetUserByID(localUserID)
  230. if err != nil {
  231. log.Error("Unable to GetUserByID %d: %s", localUserID, err)
  232. return impart.HTTPError{http.StatusInternalServerError, err.Error()}
  233. }
  234. if err = loginOrFail(h.Store, w, r, user); err != nil {
  235. log.Error("Unable to loginOrFail %d: %s", localUserID, err)
  236. return impart.HTTPError{http.StatusInternalServerError, err.Error()}
  237. }
  238. return nil
  239. }
  240. if attachUserID > 0 {
  241. log.Info("attaching to user %d", attachUserID)
  242. err = h.DB.RecordRemoteUserID(r.Context(), attachUserID, tokenInfo.UserID, provider, clientID, tokenResponse.AccessToken)
  243. if err != nil {
  244. return impart.HTTPError{http.StatusInternalServerError, err.Error()}
  245. }
  246. return impart.HTTPError{http.StatusFound, "/me/settings"}
  247. }
  248. displayName := tokenInfo.DisplayName
  249. if len(displayName) == 0 {
  250. displayName = tokenInfo.Username
  251. }
  252. tp := &oauthSignupPageParams{
  253. AccessToken: tokenResponse.AccessToken,
  254. TokenUsername: tokenInfo.Username,
  255. TokenAlias: tokenInfo.DisplayName,
  256. TokenEmail: tokenInfo.Email,
  257. TokenRemoteUser: tokenInfo.UserID,
  258. Provider: provider,
  259. ClientID: clientID,
  260. }
  261. tp.TokenHash = tp.HashTokenParams(h.Config.Server.HashSeed)
  262. return h.showOauthSignupPage(app, w, r, tp, nil)
  263. }
  264. func (r *callbackProxyClient) register(ctx context.Context, state string) error {
  265. form := url.Values{}
  266. form.Add("state", state)
  267. form.Add("location", r.callbackLocation)
  268. req, err := http.NewRequestWithContext(ctx, "POST", r.server, strings.NewReader(form.Encode()))
  269. if err != nil {
  270. return err
  271. }
  272. req.Header.Set("User-Agent", "writefreely")
  273. req.Header.Set("Accept", "application/json")
  274. req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
  275. resp, err := r.httpClient.Do(req)
  276. if err != nil {
  277. return err
  278. }
  279. if resp.StatusCode != http.StatusCreated {
  280. return fmt.Errorf("unable register state location: %d", resp.StatusCode)
  281. }
  282. return nil
  283. }
  284. func limitedJsonUnmarshal(body io.ReadCloser, n int, thing interface{}) error {
  285. lr := io.LimitReader(body, int64(n+1))
  286. data, err := ioutil.ReadAll(lr)
  287. if err != nil {
  288. return err
  289. }
  290. if len(data) == n+1 {
  291. return fmt.Errorf("content larger than max read allowance: %d", n)
  292. }
  293. return json.Unmarshal(data, thing)
  294. }
  295. func loginOrFail(store sessions.Store, w http.ResponseWriter, r *http.Request, user *User) error {
  296. // An error may be returned, but a valid session should always be returned.
  297. session, _ := store.Get(r, cookieName)
  298. session.Values[cookieUserVal] = user.Cookie()
  299. if err := session.Save(r, w); err != nil {
  300. fmt.Println("error saving session", err)
  301. return err
  302. }
  303. http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
  304. return nil
  305. }