middleware.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package ui // import "miniflux.app/v2/internal/ui"
  4. import (
  5. "context"
  6. "errors"
  7. "log/slog"
  8. "net/http"
  9. "net/url"
  10. "miniflux.app/v2/internal/config"
  11. "miniflux.app/v2/internal/crypto"
  12. "miniflux.app/v2/internal/http/cookie"
  13. "miniflux.app/v2/internal/http/request"
  14. "miniflux.app/v2/internal/http/response/html"
  15. "miniflux.app/v2/internal/http/route"
  16. "miniflux.app/v2/internal/model"
  17. "miniflux.app/v2/internal/storage"
  18. "miniflux.app/v2/internal/ui/session"
  19. "github.com/gorilla/mux"
  20. )
  21. type middleware struct {
  22. router *mux.Router
  23. store *storage.Storage
  24. }
  25. func newMiddleware(router *mux.Router, store *storage.Storage) *middleware {
  26. return &middleware{router, store}
  27. }
  28. func (m *middleware) handleUserSession(next http.Handler) http.Handler {
  29. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  30. session := m.getUserSessionFromCookie(r)
  31. if session == nil {
  32. if m.isPublicRoute(r) {
  33. next.ServeHTTP(w, r)
  34. } else {
  35. slog.Debug("Redirecting to login page because no user session has been found",
  36. slog.String("url", r.RequestURI),
  37. )
  38. loginURL, _ := url.Parse(route.Path(m.router, "login"))
  39. values := loginURL.Query()
  40. values.Set("redirect_url", r.RequestURI)
  41. loginURL.RawQuery = values.Encode()
  42. html.Redirect(w, r, loginURL.String())
  43. }
  44. } else {
  45. slog.Debug("User session found",
  46. slog.String("url", r.RequestURI),
  47. slog.Int64("user_id", session.UserID),
  48. slog.Int64("user_session_id", session.ID),
  49. )
  50. ctx := r.Context()
  51. ctx = context.WithValue(ctx, request.UserIDContextKey, session.UserID)
  52. ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true)
  53. ctx = context.WithValue(ctx, request.UserSessionTokenContextKey, session.Token)
  54. next.ServeHTTP(w, r.WithContext(ctx))
  55. }
  56. })
  57. }
  58. func (m *middleware) handleAppSession(next http.Handler) http.Handler {
  59. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  60. if mux.CurrentRoute(r).GetName() == "feedIcon" {
  61. // Skip app session handling for the feed icon route to avoid unnecessary session creation
  62. // when fetching feed icons.
  63. next.ServeHTTP(w, r)
  64. return
  65. }
  66. var err error
  67. session := m.getAppSessionValueFromCookie(r)
  68. if session == nil {
  69. if request.IsAuthenticated(r) {
  70. userID := request.UserID(r)
  71. slog.Debug("Cookie expired but user is logged: creating a new app session",
  72. slog.Int64("user_id", userID),
  73. )
  74. session, err = m.store.CreateAppSessionWithUserPrefs(userID)
  75. if err != nil {
  76. html.ServerError(w, r, err)
  77. return
  78. }
  79. } else {
  80. slog.Debug("App session not found, creating a new one")
  81. session, err = m.store.CreateAppSession()
  82. if err != nil {
  83. html.ServerError(w, r, err)
  84. return
  85. }
  86. }
  87. http.SetCookie(w, cookie.New(cookie.CookieAppSessionID, session.ID, config.Opts.HTTPS(), config.Opts.BasePath()))
  88. }
  89. if r.Method == http.MethodPost {
  90. formValue := r.FormValue("csrf")
  91. headerValue := r.Header.Get("X-Csrf-Token")
  92. if !crypto.ConstantTimeCmp(session.Data.CSRF, formValue) && !crypto.ConstantTimeCmp(session.Data.CSRF, headerValue) {
  93. slog.Warn("Invalid or missing CSRF token",
  94. slog.String("url", r.RequestURI),
  95. slog.String("form_csrf", formValue),
  96. slog.String("header_csrf", headerValue),
  97. )
  98. if mux.CurrentRoute(r).GetName() == "checkLogin" {
  99. html.Redirect(w, r, route.Path(m.router, "login"))
  100. return
  101. }
  102. html.BadRequest(w, r, errors.New("invalid or missing CSRF"))
  103. return
  104. }
  105. }
  106. ctx := r.Context()
  107. ctx = context.WithValue(ctx, request.SessionIDContextKey, session.ID)
  108. ctx = context.WithValue(ctx, request.CSRFContextKey, session.Data.CSRF)
  109. ctx = context.WithValue(ctx, request.OAuth2StateContextKey, session.Data.OAuth2State)
  110. ctx = context.WithValue(ctx, request.OAuth2CodeVerifierContextKey, session.Data.OAuth2CodeVerifier)
  111. ctx = context.WithValue(ctx, request.FlashMessageContextKey, session.Data.FlashMessage)
  112. ctx = context.WithValue(ctx, request.FlashErrorMessageContextKey, session.Data.FlashErrorMessage)
  113. ctx = context.WithValue(ctx, request.UserLanguageContextKey, session.Data.Language)
  114. ctx = context.WithValue(ctx, request.UserThemeContextKey, session.Data.Theme)
  115. ctx = context.WithValue(ctx, request.LastForceRefreshContextKey, session.Data.LastForceRefresh)
  116. ctx = context.WithValue(ctx, request.WebAuthnDataContextKey, session.Data.WebAuthnSessionData)
  117. next.ServeHTTP(w, r.WithContext(ctx))
  118. })
  119. }
  120. func (m *middleware) getAppSessionValueFromCookie(r *http.Request) *model.Session {
  121. cookieValue := request.CookieValue(r, cookie.CookieAppSessionID)
  122. if cookieValue == "" {
  123. return nil
  124. }
  125. session, err := m.store.AppSession(cookieValue)
  126. if err != nil {
  127. slog.Debug("Unable to fetch app session from the database; another session will be created",
  128. slog.String("cookie_value", cookieValue),
  129. slog.Any("error", err),
  130. )
  131. return nil
  132. }
  133. return session
  134. }
  135. func (m *middleware) isPublicRoute(r *http.Request) bool {
  136. route := mux.CurrentRoute(r)
  137. switch route.GetName() {
  138. case "login",
  139. "checkLogin",
  140. "stylesheet",
  141. "javascript",
  142. "oauth2Redirect",
  143. "oauth2Callback",
  144. "appIcon",
  145. "feedIcon",
  146. "favicon",
  147. "webManifest",
  148. "robots",
  149. "sharedEntry",
  150. "healthcheck",
  151. "offline",
  152. "proxy",
  153. "webauthnLoginBegin",
  154. "webauthnLoginFinish":
  155. return true
  156. default:
  157. return false
  158. }
  159. }
  160. func (m *middleware) getUserSessionFromCookie(r *http.Request) *model.UserSession {
  161. cookieValue := request.CookieValue(r, cookie.CookieUserSessionID)
  162. if cookieValue == "" {
  163. return nil
  164. }
  165. session, err := m.store.UserSessionByToken(cookieValue)
  166. if err != nil {
  167. slog.Error("Unable to fetch user session from the database",
  168. slog.String("cookie_value", cookieValue),
  169. slog.Any("error", err),
  170. )
  171. return nil
  172. }
  173. return session
  174. }
  175. func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
  176. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  177. if request.IsAuthenticated(r) || config.Opts.AuthProxyHeader() == "" {
  178. next.ServeHTTP(w, r)
  179. return
  180. }
  181. username := r.Header.Get(config.Opts.AuthProxyHeader())
  182. if username == "" {
  183. next.ServeHTTP(w, r)
  184. return
  185. }
  186. clientIP := request.ClientIP(r)
  187. slog.Debug("[AuthProxy] Received authenticated requested",
  188. slog.String("client_ip", clientIP),
  189. slog.String("user_agent", r.UserAgent()),
  190. slog.String("username", username),
  191. )
  192. user, err := m.store.UserByUsername(username)
  193. if err != nil {
  194. html.ServerError(w, r, err)
  195. return
  196. }
  197. if user == nil {
  198. if !config.Opts.IsAuthProxyUserCreationAllowed() {
  199. slog.Debug("[AuthProxy] User doesn't exist and user creation is not allowed",
  200. slog.Bool("authentication_failed", true),
  201. slog.String("client_ip", clientIP),
  202. slog.String("user_agent", r.UserAgent()),
  203. slog.String("username", username),
  204. )
  205. html.Forbidden(w, r)
  206. return
  207. }
  208. if user, err = m.store.CreateUser(&model.UserCreationRequest{Username: username}); err != nil {
  209. html.ServerError(w, r, err)
  210. return
  211. }
  212. }
  213. sessionToken, _, err := m.store.CreateUserSessionFromUsername(user.Username, r.UserAgent(), clientIP)
  214. if err != nil {
  215. html.ServerError(w, r, err)
  216. return
  217. }
  218. slog.Info("[AuthProxy] User authenticated successfully",
  219. slog.Bool("authentication_successful", true),
  220. slog.String("client_ip", clientIP),
  221. slog.String("user_agent", r.UserAgent()),
  222. slog.Int64("user_id", user.ID),
  223. slog.String("username", user.Username),
  224. )
  225. m.store.SetLastLogin(user.ID)
  226. sess := session.New(m.store, request.SessionID(r))
  227. sess.SetLanguage(user.Language)
  228. sess.SetTheme(user.Theme)
  229. http.SetCookie(w, cookie.New(
  230. cookie.CookieUserSessionID,
  231. sessionToken,
  232. config.Opts.HTTPS(),
  233. config.Opts.BasePath(),
  234. ))
  235. html.Redirect(w, r, route.Path(m.router, user.DefaultHomePage))
  236. })
  237. }