2026-10-03 10:51:26 -04:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/subtle"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/auth"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// CSRFHeader carries the session's CSRF token, which the client reads from
|
|
|
|
|
// GET /api/v1/me.
|
|
|
|
|
const CSRFHeader = "X-MealQuest-CSRF"
|
|
|
|
|
|
2026-10-03 11:22:53 -04:00
|
|
|
// requireCSRF passes GET requests to next; every other
|
2026-10-03 10:51:26 -04:00
|
|
|
// request must carry CSRFHeader equal to its session's CSRF token or get 403
|
|
|
|
|
// csrf. It runs behind requireSession, which puts the session in the context.
|
|
|
|
|
func requireCSRF(logger *slog.Logger) func(http.Handler) http.Handler {
|
|
|
|
|
forbidden := apiErrorHandler(logger, http.StatusForbidden, "csrf", "missing or invalid CSRF token")
|
|
|
|
|
internal := apiErrorHandler(logger, http.StatusInternalServerError, "internal", "internal error")
|
|
|
|
|
return func(next http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
switch r.Method {
|
2026-10-03 11:22:53 -04:00
|
|
|
case http.MethodGet:
|
2026-10-03 10:51:26 -04:00
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
s, ok := auth.SessionFrom(r.Context())
|
|
|
|
|
if !ok || s.CSRFToken == "" {
|
|
|
|
|
// An empty token would match a missing header.
|
|
|
|
|
logger.ErrorContext(r.Context(), "csrf check without a session token", "session", ok)
|
|
|
|
|
internal(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if subtle.ConstantTimeCompare([]byte(r.Header.Get(CSRFHeader)), []byte(s.CSRFToken)) != 1 {
|
|
|
|
|
forbidden(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|