Skip to content

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 with remember=true via jwt.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):

  1. Decode the token; type must be "refresh", user must exist and be active.
  2. If token_adapter is set, atomically consume_token(jti, "refresh_jti"):
  3. returns the row → success
  4. 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 return 401 Refresh token has already been used.
  5. 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:

auth.session_adapter = adapter.session
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/introspect and /auth/token/revoke for 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.