All checks were successful
Build and Test / build-and-test (push) Successful in 38s
1. Enforce the 12h session lifetime server-side in requireAuth (reject a session older than the TTL even if the sealed cookie is intact), so a leaked cookie value can't be replayed indefinitely. Shared sessionTTL const drives both the cookie MaxAge and the check. 2. Send X-Content-Type-Options: nosniff when serving user-uploaded receipt/attachment bytes, so the browser won't sniff past the declared (upload-time allowlisted) MIME type. Update SPEC §2 and §6 accordingly; tests cover stale-session rejection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
70 lines
1.8 KiB
Go
70 lines
1.8 KiB
Go
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
|
|
}
|
|
// 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
|
|
}
|
|
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,
|
|
})
|
|
}
|