Skip to content
Unverified — AI-generated content. Help verify this page

Token Strategies Deep Dive

Choosing a token format is one of the most consequential decisions in authentication architecture. The format determines your revocation capabilities, scalability ceiling, debuggability, and attack surface. This page compares every production-viable token strategy, from JWTs to bleeding-edge Biscuit tokens, with concrete trade-offs that matter at scale.

Token Format Comparison

JWT (JSON Web Token)

JWTs are self-contained tokens that carry claims in a signed (or encrypted) JSON payload. They are the dominant format in modern web applications.

json
{
  "header": { "alg": "ES256", "kid": "key-2026-03", "typ": "JWT" },
  "payload": {
    "sub": "user_abc123",
    "iss": "https://auth.example.com",
    "aud": "https://api.example.com",
    "exp": 1711000900,
    "iat": 1711000000,
    "roles": ["admin", "billing"],
    "tid": "tenant_xyz"
  },
  "signature": "MEUCIQDf..."
}

Strengths: Stateless validation, no database lookup, standardized (RFC 7519), widely supported.

Weaknesses: Cannot be revoked without a blocklist, claims are readable (not encrypted by default), size grows with claims, vulnerable to algorithm confusion attacks if not hardened.

Opaque Tokens

Opaque tokens are random strings with no inherent structure. All metadata lives server-side.

access_token: "kat_v1_a3f8e2c1d4b5a6f7e8c9d0a1b2c3d4e5"

The server maps this to a session record:

json
{
  "token_hash": "sha256:a3f8e2...",
  "user_id": "user_abc123",
  "scopes": ["read", "write"],
  "created_at": "2026-03-20T10:00:00Z",
  "expires_at": "2026-03-20T10:15:00Z",
  "revoked": false
}

Strengths: Instant revocation (delete the record), no information leakage, small token size, no cryptographic attack surface.

Weaknesses: Every validation requires a database/cache lookup, does not scale as easily across regions, coupling between services and token store.

Macaroons

Macaroons are bearer tokens with a unique property: they can be attenuated (restricted) by anyone who holds them, without contacting the issuer. Developed by Google Research.

identifier: "user_abc123"
location: "https://auth.example.com"
caveats:
  - "time < 2026-03-20T10:15:00Z"     (first-party caveat)
  - "ip = 203.0.113.42"                (first-party caveat)
  - "account.balance > 0 @ billing.example.com"  (third-party caveat)
signature: HMAC chain

Strengths: Caveat-based attenuation (delegate with reduced permissions), efficient HMAC chain verification, third-party caveats for distributed authorization.

Weaknesses: No standard library ecosystem (limited adoption), complex mental model, hard to debug, no widely adopted RFC.

Biscuit Tokens

Biscuit is a modern token format that combines Datalog-based authorization logic with offline attenuation and public-key cryptography. Created by Clever Cloud.

// Biscuit token authority block
authority {
  user("user_abc123");
  role("admin");
  tenant("tenant_xyz");

  // Attenuation: token holder can add restrictions
  check if time($t), $t < 2026-03-20T10:15:00Z;
  check if resource($r), operation($op),
    right($r, $op);
}

// Attenuated block (added by API gateway)
block {
  check if resource($r), $r.starts_with("/api/v2/");
  check if operation("read");
}

Strengths: Public-key crypto (no shared secrets), offline attenuation, Datalog authorization policies embedded in token, revocation by public key.

Weaknesses: Young ecosystem, limited library support, larger token size, learning curve for Datalog.

Comparison Matrix

PropertyJWTOpaqueMacaroonsBiscuit
ValidationStateless (signature)Stateful (DB lookup)Stateless (HMAC chain)Stateless (public key)
RevocationBlocklist neededDelete recordCaveat expirationRevocation ID
AttenuationNot possibleNot possibleYes (caveats)Yes (Datalog blocks)
Information leakageClaims visible (unless JWE)NoneCaveats visiblePolicy visible
Token size500B-2KB32-64 bytes200B-1KB500B-3KB
CryptoRSA/ECDSA/EdDSANone (random)HMAC chainEd25519
StandardizationRFC 7519N/AResearch paperSpecification
Library ecosystemExcellentTrivial to implementLimitedGrowing
Best forAPI auth, microservicesSimple apps, sessionsDistributed delegationPolicy-rich authorization

Access Token + Refresh Token Rotation

The access-refresh pattern is the industry standard for balancing security with user experience. Short-lived access tokens limit the damage window; long-lived refresh tokens maintain sessions without re-authentication.

Refresh Token Rotation

Every time a refresh token is used, it is revoked and a new one is issued. This is called rotation. If an attacker steals a refresh token and the legitimate user uses it first, the attacker's token is invalid. If the attacker uses it first, the legitimate user's next refresh attempt reveals the theft.

typescript
// Refresh token rotation with family tracking
interface RefreshTokenRecord {
  tokenHash: string;
  familyId: string;     // Groups all rotated tokens
  userId: string;
  used: boolean;
  createdAt: Date;
  expiresAt: Date;
}

async function rotateRefreshToken(oldToken: string): Promise<TokenPair> {
  const tokenHash = hashToken(oldToken);
  const record = await db.refreshTokens.findByHash(tokenHash);

  if (!record) {
    throw new AuthError('Invalid refresh token');
  }

  if (record.used) {
    // Reuse detected — entire family is compromised
    await db.refreshTokens.revokeFamily(record.familyId);
    await auditLog.critical('refresh_token_reuse', {
      userId: record.userId,
      familyId: record.familyId,
    });
    throw new AuthError('Token reuse detected — all sessions revoked');
  }

  if (record.expiresAt < new Date()) {
    throw new AuthError('Refresh token expired');
  }

  // Mark old token as used
  await db.refreshTokens.markUsed(tokenHash);

  // Issue new pair with same family
  const newRefreshToken = generateSecureToken();
  await db.refreshTokens.create({
    tokenHash: hashToken(newRefreshToken),
    familyId: record.familyId,
    userId: record.userId,
    used: false,
    createdAt: new Date(),
    expiresAt: addDays(new Date(), 7),
  });

  const accessToken = await issueAccessToken(record.userId);

  return { accessToken, refreshToken: newRefreshToken };
}

Reuse Detection is Critical

If a refresh token is used twice, it means either the token was stolen or there is a race condition. In either case, revoke the entire token family. This is the mechanism that makes refresh token theft detectable.

Token Binding (DPoP)

Demonstrating Proof of Possession (DPoP, RFC 9449) binds tokens to a specific client by requiring the client to prove it holds a private key. Even if an attacker steals the access token, they cannot use it without the corresponding private key.

DPoP Implementation

typescript
// Client-side: generate DPoP proof
import { generateKeyPair, SignJWT, exportJWK } from 'jose';
import { createHash, randomUUID } from 'crypto';

// Generate ephemeral key pair (once per session)
const { publicKey, privateKey } = await generateKeyPair('ES256');
const publicJwk = await exportJWK(publicKey);

async function createDPoPProof(
  method: string,
  url: string,
  accessToken?: string
): Promise<string> {
  const builder = new SignJWT({
    htm: method,                    // HTTP method
    htu: url,                       // HTTP URL
    iat: Math.floor(Date.now() / 1000),
    jti: randomUUID(),
    ...(accessToken && {
      ath: createHash('sha256')     // Access token hash
        .update(accessToken)
        .digest('base64url'),
    }),
  })
    .setProtectedHeader({
      alg: 'ES256',
      typ: 'dpop+jwt',
      jwk: publicJwk,              // Public key in header
    });

  return builder.sign(privateKey);
}

// Usage: token request with DPoP
const dpopProof = await createDPoPProof('POST', 'https://auth.example.com/token');
const response = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'DPoP': dpopProof,
  },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    // ... other params
  }),
});

When to Use DPoP

DPoP is most valuable for public clients (SPAs, mobile apps) where you cannot securely store a client secret. It prevents token theft from being useful even if an attacker extracts the access token from memory or network traffic.

Token Revocation Strategies

The fundamental challenge: how do you invalidate a stateless token?

Strategy 1: Short-Lived Tokens Only

Issue access tokens with a 5-15 minute TTL. Never revoke them — just wait for expiry.

Pros: Zero infrastructure, truly stateless
Cons: 5-15 minute window where revoked tokens still work
Best for: Low-risk APIs, internal microservices

Strategy 2: Blocklist (Deny List)

Maintain a set of revoked token IDs (JTI claims) in Redis. Check on every request.

typescript
// Redis blocklist check
async function isTokenRevoked(jti: string): Promise<boolean> {
  const revoked = await redis.get(`revoked:${jti}`);
  return revoked !== null;
}

async function revokeToken(jti: string, expiresIn: number): Promise<void> {
  // Set TTL to match token expiry — auto-cleanup
  await redis.setex(`revoked:${jti}`, expiresIn, '1');
}
Pros: Near-instant revocation, simple implementation
Cons: Every request requires a Redis lookup, Redis becomes critical path
Best for: Most production systems

Strategy 3: Token Families

Group all tokens from a single login session into a family. Revoke the entire family at once.

Pros: Logout revokes all tokens from that session, detects refresh token reuse
Cons: Requires family tracking in the database
Best for: Systems with refresh token rotation

Strategy 4: Versioned Tokens

Store a tokenVersion counter on the user record. Include the version in the JWT. If the JWT version is less than the current user version, reject it.

typescript
// Versioned token validation
async function validateTokenVersion(
  userId: string,
  tokenVersion: number
): Promise<boolean> {
  const user = await db.users.findById(userId);
  return user.tokenVersion === tokenVersion;
}

// To revoke all tokens for a user:
await db.users.incrementTokenVersion(userId);
Pros: Revoke all user tokens with one DB update, no blocklist infrastructure
Cons: Requires a DB lookup per request (partially stateful)
Best for: "Logout everywhere" functionality

Revocation Strategy Comparison

StrategyRevocation SpeedInfrastructureStatelessnessGranularity
Short-lived onlyUp to TTLNoneFully statelessNone (wait for expiry)
BlocklistInstantRedisPartially statefulPer-token
Token familiesInstantDatabaseStatefulPer-session
Versioned tokensInstantDatabasePartially statefulPer-user (all tokens)
Combined (short + blocklist)Instant for critical, TTL for routineRedisMostly statelessFlexible

Stateless vs Stateful Tokens — The Real Trade-offs

This is one of the most debated topics in auth engineering. The truth is nuanced.

The Stateless Fantasy

Fully stateless JWTs sound perfect: no database, infinite scale, validate anywhere. In practice, you almost always need some state:

  • Revocation requires a blocklist (state)
  • Refresh tokens require a token store (state)
  • Rate limiting requires counters (state)
  • Concurrent session control requires session tracking (state)
  • Audit logging requires a log store (state)

The Honest Truth

"Stateless JWTs" in production are really "mostly-stateless JWTs with a Redis blocklist." Pure statelessness means accepting that you cannot revoke a token until it expires. Few production systems can tolerate that.

When Stateless Wins

ScenarioWhy Stateless Works
Microservice-to-microservice callsServices trust the gateway; short-lived tokens; no user session to revoke
CDN/edge validationCannot call a central database from 200+ edge nodes at low latency
High-throughput read APIsEliminating a Redis call saves 1-5ms per request, which matters at 100K+ RPS

When Stateful Wins

ScenarioWhy Stateful Works
Financial applicationsMust revoke access instantly on fraud detection
Healthcare / complianceAudit trail requires knowing exactly which sessions are active
Consumer apps with "logout everywhere"Users expect immediate session termination
Admin panelsCompromised admin tokens must be revocable in seconds

Token Size Optimization

Large tokens increase bandwidth, slow mobile apps on poor connections, and can exceed cookie size limits (4KB) or header size limits (8KB in many servers).

Strategies

TechniqueSize ReductionTrade-off
Remove unnecessary claims20-40%Less information in token
Use short claim names (r instead of roles)10-20%Less readable
Use integer role IDs instead of strings15-25%Requires mapping table
Use ES256 instead of RS256~50% smaller signatureSlightly slower verification
Move large claims to a userinfo endpoint60-80%Extra API call needed
Use opaque tokens + introspection95% smallerFully stateful

Size Comparison

RS256 JWT with typical claims:     ~800 bytes
ES256 JWT with typical claims:     ~500 bytes
ES256 JWT with minimal claims:     ~300 bytes
EdDSA JWT with minimal claims:     ~280 bytes
Opaque token:                       ~44 bytes

Rule of Thumb

If your JWT exceeds 1KB, you are putting too much in it. Move large data (permissions lists, feature flags) to a userinfo or permissions endpoint that the client calls once and caches.

Further Reading

"What I cannot create, I do not understand." — Richard Feynman