navree
Documentation

Sessions and step-up

List and end a user's sessions, and ask for the password or a code again before sensitive actions with #[recent_auth].

On this page

Every sign-in starts a session: one device, one browser, one refresh-token family. Users can list their sessions and end the ones they don't recognize. Sensitive routes can also ask for proof that the person at the keyboard is still the account's owner: a recent sign-in, or the password or a code confirmed a moment ago. This page assumes AuthModule is set up as described in Authentication.

What a session is

A session is a refresh-token family. Signing in with a password, a second factor, a magic link, an emailed code or a passkey, a social provider or a trusted token starts one. Each refresh issues the next token of the same family, so the session keeps its id for as long as the client keeps refreshing.

Access tokens carry their session in two claims:

Claim
sidThe session id (the family id)
auth_timeWhen the user last proved who they are, in Unix seconds: the sign-in, or the last confirmation
JSON

A refreshed access token keeps the session's auth_time: refreshing is not signing in. Both claims are absent from tokens issued before the session columns existed; such tokens keep working, and their first refresh adds sid.

Listing sessions

GET /api/auth/sessions lists the signed-in user's live sessions, most recently used first:

HTTP
JSON
Field
idThe session id: the sid claim, and what DELETE /api/auth/sessions/{id} takes
ip, user_agentThe client of the session's last sign-in or refresh
deviceA short description from the user agent: Chrome on Android, Safari on iOS, curl; null when it isn't recognized
last_used_atThe session's last sign-in or refresh
authenticated_atThe session's last sign-in or confirmation
expires_atWhen it ends unless it is refreshed
currentWhether this request was made with it
impersonator_idFor an impersonation session, the support agent acting as the user
platformWhether it is a platform session

A session is live while its newest refresh token is neither revoked nor expired. Sessions ended by a sign-out, a password change or reset, or reuse detection don't appear.

Ending a session

DELETE /api/auth/sessions/{id} ends one of the user's sessions:

HTTP

The answer is 204 No Content. The session's refresh tokens stop working at once; its access tokens keep working until they expire (15 minutes by default), as with every sign-out. Ending the current session is a sign-out. An id that isn't one of the user's live sessions gets 404 Not Found, so nobody can probe other users' sessions. The route refuses impersonation sessions.

To end every session at once, use POST /api/auth/logout with {"all": true} (see Signing out).

Requiring a recent sign-in

Some actions deserve more than a valid token: deleting the account, exporting all data, changing billing details. A token that leaked, or a laptop left unlocked, shouldn't be enough. Put #[recent_auth(minutes = N)] on such a route:

src/modules/account/controller.rs

The route accepts a request only when its access token's auth_time is at most 10 minutes old, by the app's dyn Clock. Everything else gets a 403 with the code reauthentication_required:

JSON

That covers an older sign-in, an impersonation session (it never signed in, so it has no auth_time), and requests authenticated another way, such as a session cookie or an API key strategy. The client then confirms and retries.

  • #[recent_auth] takes between 1 and 10,080 minutes (a week); anything else is a compile error. It can sit on the impl block, where it applies to every route, and a route's own replaces the impl block's.
  • It is a guard, not an access declaration: the route still needs #[authenticated], #[roles], #[permissions] or #[can].
  • It needs the auth feature of the navree facade (or navree-auth as a dependency).

For hand-written routes, the guard is navree::auth::recent_auth:

Rust

Confirming the password or a code

POST /api/auth/confirm checks the password or a code from the authenticator app, and returns a new access token for the same session whose auth_time is now:

HTTP
JSON
  • Send exactly one of password and code. Without either, or with both, the answer is a 400 field error on password.
  • code is a current TOTP code. The account must have two-factor authentication on (400 Two-factor authentication is not enabled otherwise). Recovery codes are not accepted here.
  • A wrong password or code is a 400 field error on password or code. Wrong passwords count towards the login throttle and wrong codes towards the two-factor throttle, so confirmation can't be used to guess either.
  • The session remembers the confirmation: tokens refreshed afterwards carry the new auth_time. No new refresh token is issued; keep the one you have.
  • Impersonation sessions get 403 Not allowed while impersonating a user.

A front end usually handles reauthentication_required in one place: show a password prompt, post it to /api/auth/confirm, swap in the new access token and repeat the request.

Reading the token's claims

AccessClaims gives a handler the verified claims of the request's access token. It answers 401 when the request has none (a guest, or a request authenticated another way); take Option<AccessClaims> to make it optional.

Rust

AccessClaims dereferences to navree::auth::Claims, so claims.sub, claims.exp and the other fields are there too. The JWT strategy stores it in the request after verifying the token.

In code

AuthService methodWhat it does
sessions(user_id, current)The user's live sessions as SessionResources; the one with id current is marked current
revoke_session(user_id, session_id, &meta)Ends one session; AuthError::SessionNotFound when it isn't the user's
confirm(&claims, proof, &meta)Step-up confirmation with ConfirmProof::Password(..) or ConfirmProof::Code(..): a ConfirmedToken

JwtService::sign_session(user_id, typ, ttl, SessionClaims { session_id, auth_time, impersonator }) signs a token with session claims yourself; verify_any(token, &[types]) verifies a token of any of several types.

Audit entries

ActionKindWhen
auth.session_revokedauthA session was ended; metadata.session_id
auth.confirmedauthThe user confirmed; metadata.method (password or totp) and metadata.session_id

auth.refresh entries carry the session in metadata.family_id, as before.

The session columns

Three columns of refresh_tokens hold the session's state, copied from token to token at each refresh:

Column
authenticated_atThe session's last sign-in or confirmation: the auth_time of its tokens
impersonator_idFor an impersonation session, the impersonating user
audienceplatform for a platform session; null for the app

App sessions slide: each refresh extends them by auth.jwt.refresh_ttl_secs. Impersonation and platform sessions end at a fixed time, however often they are refreshed. The migration m20261027_000001_add_session_columns_to_refresh_tokens of the auth set adds them.

Testing

Override dyn Clock with a FixedClock to test #[recent_auth] routes: sign in, move the clock past the limit, and check that the route refuses until the client confirms.

Rust

builder and sign_in stand for your test setup and a POST /api/auth/login, as in Authentication.

Limits

  • Access tokens outlive their session. Ending a session stops its refresh tokens; an access token already issued works until it expires. Keep auth.jwt.access_ttl_secs short.
  • Devices are guesses. device comes from the user agent, which the client chooses.
  • One second factor for confirmation. POST /api/auth/confirm takes the password or a TOTP code, not a passkey.