2026-06-18 01:40:12 +00:00
|
|
|
package web
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"net/http"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"maisym.com/hsa/internal/auth"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type ctxKey int
|
|
|
|
|
|
|
|
|
|
const sessionKey ctxKey = 0
|
|
|
|
|
|
|
|
|
|
// requireAuth rejects requests without a valid session cookie by redirecting to /login.
|
|
|
|
|
// On success the decoded session is placed in the request context.
|
|
|
|
|
func (s *Server) requireAuth(next http.Handler) http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
c, err := r.Cookie(sessionCookie)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.Redirect(w, r, "/login", http.StatusFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
sess, err := auth.DecodeSession(c.Value, s.cfg.SessionKey)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.clearCookie(w, sessionCookie)
|
|
|
|
|
http.Redirect(w, r, "/login", http.StatusFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-06-21 00:56:38 +00:00
|
|
|
// Server-side expiry: a cookie's MaxAge is client-enforced, so also reject a
|
|
|
|
|
// session older than its TTL — a captured cookie value can't be replayed forever.
|
|
|
|
|
if time.Since(sess.IssuedAt) > sessionTTL {
|
|
|
|
|
s.clearCookie(w, sessionCookie)
|
|
|
|
|
http.Redirect(w, r, "/login", http.StatusFound)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-06-18 01:40:12 +00:00
|
|
|
ctx := context.WithValue(r.Context(), sessionKey, sess)
|
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sessionFrom returns the session stored in the context by requireAuth.
|
|
|
|
|
func sessionFrom(ctx context.Context) auth.Session {
|
|
|
|
|
sess, _ := ctx.Value(sessionKey).(auth.Session)
|
|
|
|
|
return sess
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) setCookie(w http.ResponseWriter, name, value string, ttl time.Duration) {
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: name,
|
|
|
|
|
Value: value,
|
|
|
|
|
Path: "/",
|
|
|
|
|
MaxAge: int(ttl.Seconds()),
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
Secure: s.cfg.SecureCookies,
|
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *Server) clearCookie(w http.ResponseWriter, name string) {
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: name,
|
|
|
|
|
Value: "",
|
|
|
|
|
Path: "/",
|
|
|
|
|
MaxAge: -1,
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
Secure: s.cfg.SecureCookies,
|
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
|
})
|
|
|
|
|
}
|