Refresh Token
Data Entity
Description
Rotating, single-use credentials that allow clients to obtain new short-lived access tokens without re-authenticating. Belongs to a parent Session and participates in a token family for reuse-attack detection. Issued by the Authentication Module; consumed by the API HTTP client on 401 responses.
Data Structure
| Name | Type | Description | Constraints |
|---|---|---|---|
id |
uuid |
Surrogate primary key. Never exposed to clients — the opaque token string is the external identifier. | PKrequiredunique |
session_id |
uuid |
Foreign key to the parent sessions row. Revoking the session cascades to all its refresh tokens. | required |
user_id |
uuid |
Denormalized reference to the owning user. Allows efficient per-user token enumeration and bulk revocation without joining through sessions. | required |
token_hash |
string |
SHA-256 hash of the opaque token string issued to the client. The plaintext token is never stored. Compared on every refresh request. | requiredunique |
token_family |
uuid |
Groups all tokens in the same rotation chain. If a previously-used token from a family is presented (reuse attack), the entire family is revoked immediately. | required |
replaced_by_id |
uuid |
Points to the successor refresh token created when this token was consumed during rotation. Null while active or if invalidated without rotation. | unique |
client_type |
enum |
The client surface that holds this token. Drives storage expectations: mobile uses the platform secure store; web_admin uses HTTP-only cookies. | required |
issued_at |
datetime |
UTC timestamp when this token was created. Used for age checks and audit trails. | required |
expires_at |
datetime |
UTC timestamp after which the token is unconditionally rejected, regardless of revocation status. Typically 30 days after issuance for mobile, 24 hours for web_admin. | required |
used_at |
datetime |
UTC timestamp when this token was consumed in a successful refresh exchange. Null while unused. Presentation of a used token triggers reuse-attack handling. | - |
revoked_at |
datetime |
UTC timestamp when this token was explicitly invalidated. Null while valid. Set on sign-out, session revocation, admin action, or reuse detection. | - |
revocation_reason |
enum |
Machine-readable cause of revocation. Used for audit log enrichment and security dashboard metrics. | - |
ip_address |
string |
IPv4 or IPv6 address of the client at token issuance. Stored for anomaly detection (IP change between issuance and use). Never used for enforcement. | - |
user_agent |
string |
Device or browser User-Agent string at issuance. Displayed on the active sessions admin page to help users recognize their own devices. | - |
created_at |
datetime |
Database row creation timestamp. Equals issued_at for tokens created during sign-in; may differ by milliseconds for rotation-issued tokens. | required |
Database Indexes
idx_refresh_tokens_token_hash
Columns: token_hash
idx_refresh_tokens_session_id
Columns: session_id
idx_refresh_tokens_user_id
Columns: user_id
idx_refresh_tokens_token_family
Columns: token_family
idx_refresh_tokens_expires_at
Columns: expires_at
idx_refresh_tokens_user_active
Columns: user_id, revoked_at, expires_at
Validation Rules
token_hash_format
error
Validation failed
expires_at_after_issued_at
error
Validation failed
revocation_fields_consistency
error
Validation failed
used_at_before_expires_at
error
Validation failed
replaced_by_id_immutability
error
Validation failed
session_id_foreign_key
error
Validation failed
ip_address_format
warning
Validation failed
Business Rules
single_use_enforcement
Each refresh token may be used exactly once. On a valid refresh request the current token is marked used_at = now(), a new token is issued in the same family, and replaced_by_id is set to the new token's id. A second presentation of the same token after used_at is set triggers the reuse_attack rule below.
reuse_attack_family_revocation
If a token whose used_at is already set is presented again, the entire token_family is revoked immediately (all rows in the family receive revoked_at = now() and revocation_reason = reuse_attack). The associated session is also terminated. An audit event is emitted.
expiry_enforcement
Tokens where expires_at < now() are rejected unconditionally at the API layer, regardless of revocation state. The client must re-authenticate through the full sign-in flow.
session_cascade_revocation
When a session is revoked (via sign-out, admin action, or forced expiry), all refresh tokens with that session_id are revoked in the same transaction with revocation_reason = session_cascade.
rotation_chain_integrity
replaced_by_id must point to a token in the same token_family. Cross-family linking is rejected. Ensures the rotation chain is auditable end-to-end.
client_type_storage_contract
Tokens with client_type = mobile are issued to be stored in the Flutter platform secure store (via secure-token-store). Tokens with client_type = web_admin are delivered only via Set-Cookie with HttpOnly + Secure + SameSite=Strict. The API never returns the raw token in a JSON response body for web_admin.
expiry_window_by_client_type
Mobile tokens expire 30 days after issuance; web_admin tokens expire 24 hours after issuance. These windows are enforced at creation time by auth-service.