-- =============================================================================
-- 2026-05-10 Audit / MED-16 closure
-- =============================================================================
--
-- Pre-login rows in oidc_pre_login_sessions used to carry only the OIDC state,
-- nonce, and PKCE verifier — the binding to the user agent that initiated the
-- handshake was implicit (the pre-login cookie's HMAC, scoped to the active
-- SessionSigningKey, only verifies that *some* caller of /auth/oidc/login is
-- talking to /auth/oidc/callback; it does not verify that the SAME browser /
-- HTTP client is on both sides).
--
-- RFC 9700 §4.7.1 (security best current practice for OAuth 2.0) recommends
-- binding state to a user-agent fingerprint + source IP so that a pre-login
-- cookie leaked in transit (CSRF / XSS / TLS termination on a shared proxy)
-- cannot be replayed by a different browser. Even with HMAC integrity, the
-- attacker who steals the bytes could otherwise complete the handshake.
--
-- This migration adds:
--   - client_ip        TEXT — captured at /auth/oidc/login from the request's
--                             clientIPFromRequest result (post LOW-5 XFF
--                             trusted-proxy gating, so the value is honest).
--   - user_agent       TEXT — captured at /auth/oidc/login from r.UserAgent().
--                             Stored verbatim; the consume path compares with
--                             constant-time equality.
--
-- Both columns are nullable so in-flight pre-login rows from pre-deploy code
-- paths still consume cleanly (the consume-side check only enforces when both
-- the row AND the request carry non-empty values; legacy rows pass through
-- because the row's binding columns are NULL).
--
-- The audit failure_category distinguishes:
--   - prelogin_ua_mismatch  — UA changed across the redirect (most common
--                             real-world false-positive: aggressive UA
--                             rewriters on enterprise proxies).
--   - prelogin_ip_mismatch  — source IP changed across the redirect (mobile
--                             carrier-grade NAT, dual-stack v4/v6 hops, VPN
--                             toggle).
--   - prelogin_uaip_mismatch — both differ.
--
-- Operators wanting to disable the gate (e.g. dual-stack v4/v6 environments
-- where source IP routinely flips) set the CERTCTL_OIDC_PRELOGIN_REQUIRE_UA
-- or CERTCTL_OIDC_PRELOGIN_REQUIRE_IP env var to "false". Default true.
-- =============================================================================

ALTER TABLE oidc_pre_login_sessions
    ADD COLUMN IF NOT EXISTS client_ip   TEXT,
    ADD COLUMN IF NOT EXISTS user_agent  TEXT;
