Skip to content
Security Contractstable

Session and token lifecycle contract

How tokens are issued, verified, stored, refreshed, and revoked: short-lived verified access tokens, rotating server-revocable refresh tokens, single-flight refresh, and secrets that never sit in plaintext.

Authentication fails quietly when tokens are trusted without verification, stored carelessly, or refreshed under a race. This spec fixes the lifecycle: what each token is, how it is verified before it is trusted, where the secret lives, and how a session is ended. Every clause closes a specific, common bypass.

Contract

Token types

TokenLifetimeRules
AccessShort (minutes)Signed, verified every request; iss/aud pinned
RefreshLong (days), rotatingServer-revocable; rotated and invalidated on use
The two tokens and their rules

Definition

Verification rule

A token is trusted only after its signature, expiry, issuer, and audience are verified. Decoding the payload proves nothing — the claims are attacker-controlled until the signature is checked. Verification failure is a rejected request; it is never a fall-through to trusting the decoded claims.

Verify — signature, expiry, issuer, audience
const { payload } = await jwtVerify(token, publicKey, {
    issuer: EXPECTED_ISSUER,      // pin who issued it
    audience: EXPECTED_AUDIENCE,  // pin that it was issued for us
});
// Only now may payload.sub / claims be trusted.

Definition

Storage rule

Tokens live in the platform's hardware-backed secure store — Keychain, Keystore/EncryptedSharedPreferences — never in a plaintext database column or an unprotected preference. Access and refresh tokens are written as a single atomic record so a suspend or crash mid-write cannot leave a torn state that logs the user out.

Contract

Refresh rule

Concurrent 401s must not each trigger a refresh. The first acquires a single-flight lock and performs the refresh; every other awaits and reuses its result. On refresh, the refresh token rotates and the previous one is invalidated server-side, so a stolen refresh token has a bounded, single-use life.

Single-flight refresh
let inFlight: Promise<Session> | null = null;

function refresh(): Promise<Session> {
    // Concurrent callers await the SAME refresh, never start a second.
    inFlight ??= performRefresh().finally(() => { inFlight = null; });
    return inFlight;
}

Definition

Revocation

A session can be ended server-side before its access token expires, via a revocation list or a per-user token version bumped on logout or credential change. Authorization for sensitive actions re-checks against current server state rather than trusting a role snapshot taken at login, so a revoked privilege takes effect before the next dangerous operation.

Invariants this spec guarantees

  • No token is trusted before its signature, expiry, issuer, and audience are verified.
  • Secrets are stored only in the hardware-backed secure store, written atomically — never in plaintext at rest.
  • Refresh is single-flight and rotates the refresh token, invalidating the prior one.
  • A revoked session is rejected before its access token would naturally expire.

Want this specified for your system?

We turn definitions like these into the actual schema, policies, and contracts your system runs on. Fixed scope, fixed price, defined delivery date.

Request a Fixed-Scope Architecture Blueprint