Files
mailgoserver/internal/webui/webmail_auth.go
T

95 lines
2.8 KiB
Go
Raw Normal View History

2026-08-12 21:14:19 +01:00
package webui
import (
"context"
"net/http"
"time"
"mailgoserver/internal/db"
)
// MailboxPrefix is the self-service webmail portal's URL prefix — a mailbox owner's
// login/account area, entirely separate from the admin dashboard at Prefix.
const MailboxPrefix = "/webmail"
const mailboxSessionCookieName = "mailgoserver_mailbox_session"
// mailboxCtxKey is its own type (not webui's ctxKey) so a mailbox session can never
// collide with or be confused for an admin session in request context — the two
// actor types are deliberately kept fully separate, per the parallel-schema design.
type mailboxCtxKey int
const ctxMailboxKey mailboxCtxKey = iota
func setMailboxSessionCookie(w http.ResponseWriter, token string, secure bool) {
http.SetCookie(w, &http.Cookie{
Name: mailboxSessionCookieName,
Value: token,
Path: "/",
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
MaxAge: int(sessionTTL.Seconds()),
})
}
func clearMailboxSessionCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{Name: mailboxSessionCookieName, Value: "", Path: "/", MaxAge: -1})
}
// currentMailboxSession loads the session + mailbox for the request's cookie, if any
// and valid. A nil session/mailbox (no error) means "not logged in".
func (a *App) currentMailboxSession(r *http.Request) (*db.MailboxSession, *db.Mailbox, error) {
c, err := r.Cookie(mailboxSessionCookieName)
if err != nil || c.Value == "" {
return nil, nil, nil
}
sess, err := a.DB.GetMailboxSession(c.Value)
if err != nil || sess == nil {
return nil, nil, err
}
if time.Now().After(sess.ExpiresAt) {
_ = a.DB.DeleteMailboxSession(sess.Token)
return nil, nil, nil
}
mbox, err := a.DB.GetMailboxByID(sess.MailboxID)
if err != nil || mbox == nil {
return nil, nil, err
}
return sess, mbox, nil
}
func mailboxFromContext(r *http.Request) *db.Mailbox {
m, _ := r.Context().Value(ctxMailboxKey).(*db.Mailbox)
return m
}
// requireMailboxAuth gates every webmail route behind a valid, fully-authenticated
// mailbox session: logged in, and second factor satisfied if one is enabled.
func (a *App) requireMailboxAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sess, mbox, err := a.currentMailboxSession(r)
if err != nil {
a.Logger.Error("mailbox session lookup: %v", err)
}
if sess == nil || mbox == nil {
http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
return
}
needsMFA := mbox.TOTPEnabled
if !needsMFA {
if n, _ := a.DB.CountMailboxWebAuthnCredentials(mbox.ID); n > 0 {
needsMFA = true
}
}
if needsMFA && !sess.MFAVerified {
http.Redirect(w, r, MailboxPrefix+"/login/mfa", http.StatusFound)
return
}
ctx := context.WithValue(r.Context(), ctxMailboxKey, mbox)
next.ServeHTTP(w, r.WithContext(ctx))
})
}