Skip to content

Token lifecycle ​

Each session begins with a random opaque token. Rotation replaces the active credential while retaining consumed-token history for replay detection.

Creation ​

createSession() generates 32 random bytes, encodes them as base64url, and hashes the resulting string with SHA-256. The store receives the hash, user UUID, optional session name, and an expiration of Date.now() + expiryTTL.

Both SQL adapters insert the family and initial token in one transaction. A failed insert rolls back the attempt, leaving no partial family. The raw token is returned only after the store operation succeeds.

Validation ​

validateRefreshToken() hashes the supplied token and checks the stored record in this order:

ConditionStatusSide effect
Token not foundmissingNone
Family revokedrevokedNone
Token consumed outside graceconsumedRevoke its family
Invalid or elapsed expirationexpiredNone
Token consumed within grace and not expiredvalidReturn userId and familyId
All checks passvalidReturn userId and familyId

Validation can mutate state

Validating a consumed refresh token at or after the grace boundary revokes its family. This method is not a read-only token inspection API.

Rotation ​

text
Family A
  R1 [consumed] → R2 [consumed] → R3 [active]

rotateRefreshToken() first validates the old token, generates a new credential, then calls consumeAndReplace(). Success returns status: 'rotated', the raw replacement refreshToken, and userId.

The replacement expires at the rotation time plus expiryTTL. Rotation does not enforce an absolute family lifetime.

If validation fails, rotation returns the same failure status and does not issue a replacement.

A consumed-token retry within reuseGraceTTL (default 30,000 milliseconds) returns { status: 'grace', userId } without another successor. The window starts at the original consumption timestamp and never slides on retries. Expiration and family revocation still apply. Set reuseGraceTTL: 0 to disable grace. Callers must preserve the replacement from the successful rotation; the store retains only hashes and cannot recover a replacement after a lost response.

A retry timeline ​

With a 30-second grace window, assuming the token has not expired and its family has not been revoked:

Time since R1 was consumedOperationResult
0 secondsSuccessful R1 rotationrotated, returns R2
5 secondsRotate R1 againgrace, no replacement
20 secondsValidate R1valid, with userId and familyId
Exactly 30 secondsRotate or validate R1consumed, revokes the family
After family revocationValidate R2revoked

Validation deliberately maps an eligible grace retry to valid; rotation exposes grace separately. A valid validation result therefore does not mean the token is unconsumed. Only rotated includes a new refresh credential. The grace window does not prove that a retry came from the original client.

Replay detection ​

If a previously consumed token is presented at or after the grace boundary, the family is revoked:

text
Replay R1 outside grace → consumed detected → Family A revoked
                               └─ R3 can no longer renew

Keep consumed history for active families. Removing it would turn a known consumed token into a missing record and weaken replay detection. The cleanup API preserves that history for retained families.

Concurrent requests ​

Both SQL adapters conditionally update the predecessor only while consumedAt is null, then read its family and insert the replacement in the same transaction. If insertion fails, consumption is rolled back. A losing update cannot create another successor or overwrite the original consumption timestamp.

Both requests may pass preflight validation, but only one conditional update can consume the predecessor. The losing attempt raises TokenAlreadyConsumedError; Core rechecks current token state and returns grace within the window or enforces reuse revocation outside it. Missing, revoked, and expired results are also respected on recheck.

The conditional update prevents duplicate successors through the adapter operation. A conditional no-op update locks the active family before token consumption. Family and user-wide revocation update the same row: revocation ordered first prevents rotation; rotation ordered first commits before revocation covers its successor. A revoked family raises SessionRevokedError, which the core maps to revoked. Expiration is rechecked under the family lock using a fresh application-clock timestamp as the consumption decision point. At or after expiry, TokenExpiredError maps to expired without consuming the token or creating a successor. No expiration grace applies; expiry after the decision does not abort commit. Custom adapters must implement exclusive consumption and signal consumed-token conflicts with TokenAlreadyConsumedError.

See Policies & Guarantees for at-most-once consumption, atomic rotation, revocation ordering, successor uniqueness, and failure atomicity. A losing concurrent request does not itself establish credential theft; how to respond is a separate policy decision.

Built for explicit authentication boundaries. Powered by VitePress.