Skip to main content

Token Lifecycle

How tokens are born, live, and die in fulla: the three token types, what is actually stored, how refresh rotation detects theft, and every knob that controls a lifetime. For the storage schema behind this, see Data Persistence; for the HTTP contract, see the API Reference and ADR-0004.

1. Three token types, three different shapes

TokenShapeValidated byLifetime default
Access tokenOpaque random string (generateSecureToken)Server-side state (introspection / userinfo) — never a self-contained JWT3600 s (access_token_ttl)
Refresh tokenOpaque random string + family idServer-side state; rotation on every use30 days (configurable)
id_tokenRS256 JWT signed via JWKS (kid published at /.well-known/jwks.json)Client-side signature verification (standard OIDC)Same as access token; issued only with the openid scope

Design rationale (ADR-0004): opaque access tokens keep the server in full control — revocation is immediate and stateful, and no key-compromised token outlives its server-side record. The JWT capability is reserved for the OIDC id_token, where the protocol requires it.

One frequent confusion: the roles that accompany a token response are part of the JSON envelope ("roles": [...]) and the id_token claims — the opaque access token itself carries nothing that needs decoding.

2. Issuance paths

All grants converge on TokenService (libs/oauth2):

GrantMintsNotes
authorization_code (+ PKCE, mandatory for PUBLIC clients)access + refresh (+ id_token if openid)The code is single-use with atomic consume (see §5)
refresh_tokennew access + new refreshOld refresh is revoked; family id is inherited (§4)
client_credentialsaccess only (M2M, no user)CONFIDENTIAL clients only
device_codeaccess + refresh after user approvalPolling per RFC 8628

expires_in in every response advertises the configured access-token lifetime — not a hardcoded 3600 (RFC 6749 §5.1).

3. What is stored (and what is not)

  • Access and refresh tokens are persisted only as SHA-256 hashes (hashToken() before any repository write) — a database dump exposes no usable credentials (ADR-0004).
  • The refresh token row additionally stores its token_family id, the associated access-token hash, revoked/revoked_at/revoked_by.
  • Rows past their TTL are deleted by OAuth2CleanupService (cleanup_interval_seconds, default 3600 s) — Postgres via periodic DELETE, Redis historically via TTL, memory via periodic sweep.

4. Refresh rotation, reuse detection, family revocation

Every refresh grants a new refresh token that inherits the same token_family (V008). If a refresh token that was already revoked is presented again, that is treated as theft: the server cascades revocation to every token in the family — attacker and legitimate user alike must re-authenticate. The full threat-model walkthrough (with a sequence diagram) lives in Security Architecture §7.

5. Single-use of authorization codes

consumeAuthCode is atomic per backend: Postgres uses UPDATE ... WHERE consumed = false ... RETURNING, Redis used a Lua script, memory uses a mutex — so a stolen code replayed in a race loses, always. The contract is enforced across all three implementations by GrantRepositoryContractTest (ctest -L Contract).

6. Revocation surfaces

SurfaceGranularity
POST /oauth2/revoke (RFC 7009)The presented token
Admin API DELETE /api/admin/tokens/{prefix}By token prefix
Admin token managementRevoke by client or by user — wipes every token minted for that principal

Revocation is stateful and immediate: the very next introspection or userinfo call with a revoked token returns {"active": false} / 401.

7. Cache interplay

With the Redis L2 cache enabled (cache.enabled), token and client reads hit fulla:cache:* before Postgres. Every write path (issue, refresh, revoke) invalidates through delayed double-delete (immediate DEL + a second DEL ~200 ms later, cache.invalidation_double_delete_delay_ms), closing the race where a concurrent reader re-fills a stale entry between the write and the first delete. Failures of the second delete are counted in fulla_cache_invalidation_failures_total — see Observability. Details in Data Persistence · cache consistency.

8. Configuration reference

KeyDefaultMeaning
access_token_ttl3600Access-token lifetime (seconds); advertised in expires_in
refresh-token TTL30 daysRefresh-token lifetime
cleanup_interval_seconds3600How often OAuth2CleanupService purges expired rows
cache.enabled / cache.ttl_secondsfalse / —L2 cache for token/client reads (Configuration Guide)