diff --git a/crates/auth-core/src/sessions.rs b/crates/auth-core/src/sessions.rs index 6a8983c..3950422 100644 --- a/crates/auth-core/src/sessions.rs +++ b/crates/auth-core/src/sessions.rs @@ -174,8 +174,10 @@ pub struct Login<'a> { pub user_id: &'a str, pub ip: Option<&'a str>, pub user_agent: Option<&'a str>, - /// The cookie the login request presented, if any: revoked before - /// the new session exists (fixation). + /// The raw cookie the login request presented, if any: the session it + /// names is revoked before the new one exists (fixation). `None` on a + /// `form_post` callback, where the `SameSite=Lax` cookie does not arrive, + /// so the revoke does not fire there (see [`issue`]; auth #36). pub presented_cookie: Option<&'a str>, /// Authentication-method references (RFC 8176) for this login — /// `passkey` methods say `["user","passkey"]`-style values when the @@ -204,9 +206,21 @@ fn audit(action: &str, user_id: &str) { } /// Issues a session on a successful login: 32 random bytes in the -/// cookie, only the SHA-256 stored, expiry `now + 30 days`. Any -/// session the request presented is revoked first — a new session is -/// always issued at login, so a fixed pre-login value dies here. +/// cookie, only the SHA-256 stored, expiry `now + 30 days`. +/// +/// **Fixation defence, and its one blind spot.** A session whose raw +/// cookie the request presents ([`Login::presented_cookie`]) is revoked +/// first, so a fixed pre-login value dies here. This does not fire on a +/// `form_post` callback (Apple): the session cookie is `SameSite=Lax` and +/// does not arrive on a cross-site POST, so `presented_cookie` is `None` +/// there. That is not a hole — the cookie is `__Host-` and `HttpOnly`, so +/// it cannot be planted cross-site, which is the attack the revoke exists +/// for; the browser has also just overwritten it with the new value. The +/// only residue is the old server-side row living until it expires. Firing +/// the revoke on every path needs the session **id** sealed into the flow +/// at `/start` and a `presented_session_id` on [`Login`] +/// ([auth #36](https://github.com/Factory-Zero/auth/issues/36)); until then, +/// do not read this as "always revokes". /// /// # Errors ///