Skip to content
This repository was archived by the owner on Oct 5, 2026. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions crates/auth-core/src/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
///
Expand Down
Loading