Sessions
This page explains how auth keeps a browser signed in: the session row and its cookie, how a sign-in creates or re-authenticates one, how a passkey confirms it, and how it ends. Read it before changing anything that signs a browser in or out, or checks how it signed in.
The code is in apps/auth-service/src/security/session.rs (Session and its
extractors), src/sessions (listing and signing out, for the settings and the
CLI) and crates/database/src/repositories/sessions.rs (SessionRepository).
The session and its cookie
A session is one row of sessions: its account, the methods that authenticated
it, when it signed in (authenticated_at), when a passkey last confirmed it
(passkey_confirmed_at) and with which passkey (passkey_id), its absolute
expiry, and the coarse browser family and IP address it was last seen from.
The browser holds a random secret in the __Host-sourceorigin_session cookie:
host-only, Path=/, Secure, HttpOnly, SameSite=Lax, for 30 days
(BrowserCookie in src/security/cookie.rs). Only the secret’s SHA-256 digest is
stored, in browser_session_secrets. A cookie that is malformed, or sent twice,
counts as absent, so an injected cookie can never shadow the browser’s own.
Session::optional finds the session by the cookie’s digest, if it is
unrevoked, unexpired and its account active. The extractors (OptionalSession,
Session, Confirmed) and authorization requests resolve it with
Session::resolve, which also records its activity: last_seen_at and its IP
are written at most once every five minutes (LAST_SEEN_INTERVAL), so reads
rarely become writes.
Signing in
Every sign-in calls Session::sign_in inside the transaction of the operation
that proved who the browser is, with the Credential it proved it with:
| Credential | From | Confirms the session |
|---|---|---|
Passkey | a passkey assertion (src/passkey), or restoring an account scheduled for deletion | yes |
Probation | the passkey of a recovery still in probation | no |
Federation | a GitHub or Google sign-in (src/federation) | no |
sign_in first locks the account, which must be active, so a sign-in and a
disablement serialize. Then it reads the browser’s current session:
- None, or another account’s: it revokes that session
(
RevocationReason::AccountSwitched) and creates a new one, which expires 30 days later. A passkey credential confirms it at once, so an action right after signing in asks for nothing more. - This account’s: it re-authenticates that session instead (see below), so
the session ID, which tokens carry as
sid, stays the same.
Either way it records SignedIn and returns a new secret. The caller sets the
cookie only after its transaction commits.
Two sign-ins differ:
- A recovery link signs in with
Session::sign_in_anew, which always revokes the browser’s session and creates a new one, so an existing session can neither make it fail nor carry over. - A passkey sign-in on an app’s own page (
src/passkey/related.rs) creates a session withSession::create_inline, which has no browser secret and so no cookie: only the tokens the app gets from it use it. It is listed and signed out like any other.
Re-authenticating and confirming
Session::reauthenticate records a new authentication of an existing session.
What it may do depends on the credential:
- A passkey confirms the session:
passkey_confirmed_atbecomes now, the passkey is recorded, andpasskeyjoins its methods. - A provider, or a recovery’s passkey in probation, renews
authenticated_at, but only while no passkey has confirmed the session, since either would lower itsacr. Otherwise it is refused, withpasskey_required.
Every re-authentication replaces the browser secret, so the cookie from before stops working, and a copy of it never gains the new authentication.
A settings change that needs a fresh confirmation runs the confirmation ceremony
at /account/passkeys/confirm/start and /finish (src/passkey/confirmation.rs).
The ceremony is bound to the session that started it, and its finish
re-authenticates that session with the passkey and records PasskeyConfirmed. A
recovery’s passkey in probation never confirms.
Authentication claims
SessionAuthentication::from(&StoredSession) derives how a session
authenticated, as tokens report it:
acrisphronce a passkey confirmed the session, andurn:sourceorigin:acr:socialbefore.auth_timeis the last passkey confirmation, or else the sign-in.amris the union of its methods’ values:pop,mfaanduserfor a passkey,fedfor a provider.
The age counts from the confirmation itself, so using a confirmation never extends it. Token issuance, authorization requirements and auth’s own step-up checks all read this one type. An authorization code copies it when it is issued, and a refresh family copies it from its code, so a grant keeps reporting the authentication it started with (authorization flow). The OIDC reference lists the claims as apps see them: authentication claims.
Confirmed<MAX_AGE> extracts a session whose confirmation is at most MAX_AGE
seconds old, five minutes by default (CONFIRMATION_MAX_AGE), checked with
StepUpRequirement::passkey_within, the rule bearer-protected resources apply.
Otherwise it answers 403 with step_up_required, and the page confirms, then
retries. The operation checks the confirmation again under the account lock
(Owner::lock_confirmed), so a session revoked meanwhile cannot finish it.
Expiry and revocation
A session expires 30 days after it was created. Re-authenticating never extends it, and neither do the refresh tokens issued from it, which expire with it. Expiry is passive: the lookups ignore an expired session, and nothing is recorded or sent.
Ending a session early has one path: DatabaseTransaction::revoke_sessions. It
revokes the account’s sessions in a SessionScope (one, every other one, or all),
revokes the refresh families issued from them, and records one SessionRevoked
per session with its RevocationReason. The repository method underneath is
private to the database crate, so no revocation can skip the event. Its callers:
Session::revoke, attributed to the request’s own session: signing out, and the account switch of a sign-in.AccountService’s session operations (src/sessions), which sign one session or every other one out from the settings, the admin dashboard andauth-service account <user> sessions. They need a session but no passkey confirmation: a user who suspects a stolen cookie must be able to act at once.- Changes that end the account’s other sessions: removing a passkey
(
src/passkey/settings.rs), unlinking an identity (src/federation/identities.rs) and reporting a history entry as not the owner’s (src/history). Undoing a recovery (src/recovery) ends every session. Session::end_all, for an account disabled, scheduled for deletion, purged or recovered, and forAdminService::revoke_user_sessions, which the dashboard andauth-service session revoke-allrun: every session, then every refresh grant of the account, those of sessions that already expired included.
Signing out (AccountService::sign_out) ends the browser’s session and clears
its cookie. RP-initiated logout signs out the same way, recording the client
that asked (logout).
Back-channel logout
Recording SessionRevoked queues the logout tokens of the session’s apps, in
the same transaction. A trigger records each client that redeemed a code from the
session in oidc_session_clients, and the backchannel_logout channel creates
one delivery per such client that registered a back-channel logout URI. The
delivery workers sign and post each token outside the request, retrying for up
to an hour until the app accepts it. So every app of a revoked session is sent a
logout token, and no app of an expired one is.
The delivery queue describes the channel, and back-channel logout what an app receives.
The admin dashboard’s session
The admin dashboard, on its own origin, keeps a session of its own that hangs off
an auth session (DashboardSignIn in src/admin/sign_in.rs):
- Auth’s handoff page hands a session signed in with a passkey, of an account
that holds administrator rights, to the dashboard with a code that works once,
for 60 seconds, bound to the dashboard’s state cookie
(
__Host-sourceorigin_dashboard_state). - Redeeming the code creates a
dashboard_sessionsrow naming the auth session, and the__Host-sourceorigin_dashboardcookie on the dashboard’s origin, whose digest alone is stored. - Every request finds the dashboard session only while its auth session is
active, and checks again that the account holds administrator rights and that
the auth session’s
acrisphr.
So signing out of auth, revoking the auth session or replacing it ends the dashboard’s too, while signing out of the dashboard ends only its own. The dashboard keeps its own confirmation time, set only by a passkey proof made for it, and a change needs that confirmation from the last five minutes; the auth session’s confirmation never counts. admin.sourceorigin.dev describes the handoff and confirmation from the administrator’s side.