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.
This commit is contained in:
parent
93ec8b8131
commit
b4222077b1
1 changed files with 21 additions and 5 deletions
|
|
@ -25,6 +25,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/audit"
|
"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
|
// (SPEC-006 §8). The door wires it to setup mode, where a passkey is
|
||||||
// useless. nil = the nudge runs (subject to the other conditions).
|
// useless. nil = the nudge runs (subject to the other conditions).
|
||||||
SuppressNudge func() bool
|
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 marks cookies Secure (https).
|
||||||
Secure bool
|
Secure bool
|
||||||
// Logger receives structured logs. Defaults to slog.Default().
|
// 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).
|
// 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 {
|
func (a *Actions) Logout(w http.ResponseWriter, r *http.Request, guid string) error {
|
||||||
ctx := r.Context()
|
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 {
|
if err := a.d.Sessions.Revoke(ctx, c.Value); err != nil {
|
||||||
a.d.Logger.Warn("flow: logout revoke failed", "error", err)
|
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)
|
http.Redirect(w, r, "/home", http.StatusSeeOther)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -481,7 +487,7 @@ func (a *Actions) establishSessionAt(w http.ResponseWriter, r *http.Request, idn
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 == "" {
|
if target == "" {
|
||||||
target = a.successTarget(idn.GUID)
|
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() {
|
if a.d.SuppressNudge != nil && a.d.SuppressNudge() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if httpx.IsLoopbackHost(r.Host) {
|
if httpx.IsLoopbackHost(a.originHost(r)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if a.d.Users == nil {
|
if a.d.Users == nil {
|
||||||
|
|
@ -534,6 +540,16 @@ func (a *Actions) shouldNudge(ctx context.Context, r *http.Request, idn auth.Ide
|
||||||
return !hasPasskey
|
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) ──────────────────────────────────
|
// ── the passkey nudge (SPEC-006 §8, D-041) ──────────────────────────────────
|
||||||
|
|
||||||
// PasskeyPrompt renders the passkey nudge screen. It is a session-gated ROUTE
|
// 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 {
|
if m, ok := audit.MetaFrom(ctx); ok {
|
||||||
ip = m.ClientIP
|
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 {
|
if old, err := a.d.Sessions.Validate(ctx, c.Value); err == nil && old.IdentityID == idn.ID {
|
||||||
return a.d.Sessions.Rotate(ctx, c.Value)
|
return a.d.Sessions.Rotate(ctx, c.Value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue