Tokens and sessions¶
FastAuth's session model is JWT access + refresh token pairs. The optional /auth/sessions endpoints track server-side session rows separately, and are independent of the JWT pair.
The token pair¶
On register, login, refresh, OAuth callback (sign-in), magic-link callback, and passkey authenticate/complete, FastAuth issues:
- an access token (
type: "access", default TTL 15 min,jwt.access_token_ttl), - a refresh token (
type: "refresh", default TTL 30 days,jwt.refresh_token_ttl; or 90 days withremember=trueviajwt.remember_me_ttl).
Both share the claim set {sub, jti, iss?, aud?, iat, exp, type, email, name, email_verified}. See JWT for the full claim reference.
Tokens are returned in the JSON body (token_delivery="json", default) or set as HttpOnly cookies (token_delivery="cookie").
Refresh rotation and replay detection¶
POST /auth/refresh reads the refresh token from the request body OR the refresh cookie (running the CSRF check when it comes from the cookie):
- Decode the token;
typemust be"refresh", user must exist and be active. - If
token_adapteris set, atomicallyconsume_token(jti, "refresh_jti"): - returns the row → success
- returns
None→ the token was reused or never existed. Revoke the entire refresh-token family for the user (delete_user_tokens(user_id, "refresh_jti")) and return401 Refresh token has already been used. - Issue a new pair; record the new JTI.
So a leaked-and-replayed refresh token causes the legitimate holder's next refresh to fail — both sides are forced to re-authenticate.
JTI allowlist¶
On every token issuance, FastAuth writes a TokenAdapter row of type "refresh_jti" with expires_at = exp. This is the "allowlist". Revocation via /auth/token/revoke, /auth/logout, /auth/reset-password, and /auth/account/change-password deletes rows from this list.
Disable it (tests/demos only):
config = FastAuthConfig(
...,
token_adapter=adapter.token,
require_token_adapter_for_refresh=False, # tests only
)
This disables revocation, replay detection, and password-reset revocation. Never use in production.
/auth/sessions¶
The three /auth/sessions endpoints are mounted unconditionally, but each returns 400 "Session management is not configured" until you assign:
| Method | Path | Response |
|---|---|---|
| GET | /auth/sessions |
[{id, user_id, ip_address?, user_agent?}] |
| DELETE | /auth/sessions/all |
{message} |
| DELETE | /auth/sessions/{session_id} |
{message} (404 if not found/owned) |
These session rows are separate from the JWT pair. Login does not currently create a SessionData row — the rows are produced when you use DatabaseSessionBackend with a SessionAdapter. FastAuth does not insert session rows on login today; tracking sessions on login is a per-application concern. (needs maintainer clarification on whether login will populate SessionData in a future release)
Token / session relationships¶
flowchart LR
LOGIN["POST /auth/login"] --> PAIR[token pair]
PAIR -->|token_adapter| JTI[refresh_jti allowlist]
PAIR -->|cookie mode| COOKIES["access/refresh/csrf cookies"]
REFRESH["POST /auth/refresh"] --> CONSUME[consume JTI atomically]
CONSUME -->|missing| REVOKEREPLAY["revoke family -> 401"]
CONSUME -->|ok| NEWPAIR[issue new pair]
SESSIONS["/auth/sessions"] --> SA[SessionAdapter]
SA --> SESSROWS[fastauth_sessions rows]
Choosing a session strategy¶
- JWT only (default) — no
session_adapter. Stateless. Use/auth/token/introspectand/auth/token/revokefor token validation/revocation. - JWT + listed sessions — set
auth.session_adapter = adapter.session. Lets users list and revoke their own sessions via/auth/sessions/*. The JWT pair still works exactly the same. session_strategy="database"— reserved, not currently wired. Do not rely on it.