Tokens: introspection, revocation, JTI allowlist¶
FastAuth issues JWT access + refresh token pairs. When token_adapter is configured, the JTI of every refresh token is recorded in an allowlist; refresh consumes the JTI atomically and issues a fresh pair, so reuse of an already-consumed refresh token revokes the entire family.
The JTI allowlist¶
On every token issuance in _issue_tracked_tokens, FastAuth:
- Decodes the freshly minted refresh token to read
jtiandexp. - Writes a
TokenAdapterrow of type"refresh_jti"withexpires_at = exp.
On every refresh:
- Decodes the refresh token.
typemust be"refresh". - Loads the user via
get_user_by_id. Inactive users get401. - Calls
token_adapter.consume_token(jti, "refresh_jti")atomically. If it returnsNonethe token has already been used or never existed → revoke the entire family (delete_user_tokens(user_id, "refresh_jti")) and return401 "Refresh token has already been used".
So a leaked-then-replayed refresh token causes the legitimate holder's next refresh to fail — both sides are forced to re-authenticate.
Endpoints¶
# Introspection (RFC 7662-inspired; never raises 4xx — returns active=false on bad tokens)
curl -X POST http://localhost:8000/auth/token/introspect \
-H "Authorization: Bearer $ACCESS" \
-H "Content-Type: application/json" \
-d '{"token":"..."}'
# -> {"active": true, "sub": "...", "exp": ..., "jti": "...", "token_type": "access|refresh", "email": "..."}
# Revocation (RFC 7009-inspired; refresh tokens only; caller must be owner)
curl -X POST http://localhost:8000/auth/token/revoke \
-H "Authorization: Bearer $ACCESS" \
-H "Content-Type: application/json" \
-d '{"token":"<refresh_token>"}'
/auth/token/introspect for a refresh token additionally checks the JTI against the allowlist; an expired or revoked-but-consumed JTI returns active=false.
/auth/token/revoke returns 400 if the token is not a refresh token, and 403 if the caller's user ID does not match the token's sub.
Statelessly disabling token_adapter (tests only)¶
config = FastAuthConfig(
...,
token_adapter=adapter.token,
require_token_adapter_for_refresh=False, # tests only — disables revocation/replay protection
)
This is only for tests/demos. Setting it removes logout revocation, replay detection, and password-reset password-revocation. Never use it in production.
Common mistakes¶
401 Refresh token has already been used— your client reused a refresh token. Treat refresh responses like one-time-use; always store the new refresh token and discard the old one.- Bearer at
/auth/token/introspect— the introspect endpoint requires an authenticated access token, not the token being introspected. - Sending a refresh token via Bearer —
/auth/refreshreads it from the request body OR the refresh cookie, not from theAuthorizationheader.