middleware.go 7.6 KB

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