From b4222077b18b8b1b1ad5f93f148ece6283ea348b Mon Sep 17 00:00:00 2001 From: Claus Lohmar Date: Wed, 14 Oct 2026 11:29:58 +0000 Subject: [PATCH] fix(auth): judge the passkey nudge by the configured origin REVIEW-001 (M0) L7c: shouldNudge trusted the request Host, so a reverse proxy that rewrites Host wrongly suppressed (or enabled) the nudge. Prefer the configured server.base_url host (Deps.BaseURL) and fall back to the request Host only when it is unset. Wire the secure session-cookie name through SessionCookieNameFor so the `__Host-` prefix (L1s) is used consistently by the flows. --- internal/core/auth/flow/flow.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/internal/core/auth/flow/flow.go b/internal/core/auth/flow/flow.go index 545bf1d..ec1acf8 100644 --- a/internal/core/auth/flow/flow.go +++ b/internal/core/auth/flow/flow.go @@ -25,6 +25,8 @@ import ( "fmt" "log/slog" "net/http" + "net/url" + "strings" "time" "git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/audit" @@ -71,6 +73,10 @@ type Deps struct { // (SPEC-006 §8). The door wires it to setup mode, where a passkey is // useless. nil = the nudge runs (subject to the other conditions). SuppressNudge func() bool + // BaseURL is the CONFIGURED public origin. It decides the nudge's origin + // signal: a proxy that rewrites `Host` must not flip the decision (L7c). + // Empty falls back to the request `Host`. + BaseURL string // Secure marks cookies Secure (https). Secure bool // Logger receives structured logs. Defaults to slog.Default(). @@ -385,12 +391,12 @@ func (a *Actions) LoginFinish(w http.ResponseWriter, r *http.Request, guid strin // Logout revokes the current session and clears the cookie (SPEC-006 §5). func (a *Actions) Logout(w http.ResponseWriter, r *http.Request, guid string) error { ctx := r.Context() - if c, err := r.Cookie(httpx.SessionCookieName); err == nil && c.Value != "" { + if c, err := r.Cookie(httpx.SessionCookieNameFor(a.d.Secure)); err == nil && c.Value != "" { if err := a.d.Sessions.Revoke(ctx, c.Value); err != nil { a.d.Logger.Warn("flow: logout revoke failed", "error", err) } } - httpx.ClearSessionCookie(w, httpx.SessionCookieName, a.d.Secure) + httpx.ClearSessionCookie(w, httpx.SessionCookieNameFor(a.d.Secure), a.d.Secure) http.Redirect(w, r, "/home", http.StatusSeeOther) return nil } @@ -481,7 +487,7 @@ func (a *Actions) establishSessionAt(w http.ResponseWriter, r *http.Request, idn if err != nil { return err } - httpx.SetSessionCookie(w, httpx.SessionCookieName, sess.ID, a.d.Secure) + httpx.SetSessionCookie(w, httpx.SessionCookieNameFor(a.d.Secure), sess.ID, a.d.Secure) if target == "" { target = a.successTarget(idn.GUID) } @@ -512,7 +518,7 @@ func (a *Actions) shouldNudge(ctx context.Context, r *http.Request, idn auth.Ide if a.d.SuppressNudge != nil && a.d.SuppressNudge() { return false } - if httpx.IsLoopbackHost(r.Host) { + if httpx.IsLoopbackHost(a.originHost(r)) { return false } if a.d.Users == nil { @@ -534,6 +540,16 @@ func (a *Actions) shouldNudge(ctx context.Context, r *http.Request, idn auth.Ide return !hasPasskey } +// originHost is the origin the nudge is judged against (L7c): the CONFIGURED +// base_url's host when set — so a reverse proxy that rewrites `Host` cannot +// wrongly suppress (or enable) the nudge — falling back to the request `Host`. +func (a *Actions) originHost(r *http.Request) string { + if u, err := url.Parse(strings.TrimSpace(a.d.BaseURL)); err == nil && u.Host != "" { + return u.Host + } + return r.Host +} + // ── the passkey nudge (SPEC-006 §8, D-041) ────────────────────────────────── // PasskeyPrompt renders the passkey nudge screen. It is a session-gated ROUTE @@ -646,7 +662,7 @@ func (a *Actions) issueSession(r *http.Request, idn auth.Identity) (auth.Session if m, ok := audit.MetaFrom(ctx); ok { ip = m.ClientIP } - if c, err := r.Cookie(httpx.SessionCookieName); err == nil && c.Value != "" { + if c, err := r.Cookie(httpx.SessionCookieNameFor(a.d.Secure)); err == nil && c.Value != "" { if old, err := a.d.Sessions.Validate(ctx, c.Value); err == nil && old.IdentityID == idn.ID { return a.d.Sessions.Rotate(ctx, c.Value) }