Expand description
Refresh-token rotation with replay detection.
Sessions are minted by a TokenMinter; refresh
tokens are the stateful half — short-lived access tokens are exchanged
through a long-lived refresh token that rotates on every use. Each rotation:
- consumes the presented token,
- issues a fresh successor sharing the same
chain_id, - links the successor back to its parent.
If a consumed token is presented again, that’s a replay — the entire
chain is revoked. The pattern (audited in Rauthy
src/data/src/entity/sessions.rs) keeps a stolen refresh token from
being usable indefinitely: the moment either the legitimate client or
the attacker rotates it, the next rotation by the other party trips the
replay path and severs both.
let rotator = RefreshRotator::new(store, /* ttl_seconds */ 60 * 60 * 24 * 30);
let root = rotator.mint_root(UserId::new("u1"), DeviceId::new("d1"), 1_700_000_000).await?;
let next = rotator.rotate(root.token.as_str(), 1_700_000_001).await?;
assert_eq!(next.record.chain_id, root.record.chain_id); // successor shares the chainStructs§
- ChainId
- Identifier shared by every token in a single rotation chain.
- Refresh
Rotator - Mint and rotate refresh tokens against a
RefreshStore. - Refresh
Token - Opaque refresh-token secret. 32 random bytes, base64url-encoded for
transport. This is the client-facing wire value; only its
[
hash_token] digest is persisted inRefreshTokenRecord::token. - Rotated
- Result of
RefreshRotator::mint_root/RefreshRotator::rotate.
Constants§
- CHAIN_
ID_ BYTES - Length of the chain identifier in bytes (128 bits — uniqueness only, not a secret).
- REFRESH_
TOKEN_ BYTES - Length of the opaque refresh secret in bytes (256 bits of CSPRNG).