# JWT tokens have a reasonable expiry if used

- **Pattern:** `ab-002220` (`saas-authentication.social-auth.jwt-reasonable-expiry`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002220
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002220)

## Why it matters

JWTs are stateless — they cannot be individually revoked before their expiry timestamp. A token with a 365-day lifetime handed to a compromised client, leaked via a log line, or obtained through an XSS payload is valid for a year with no mechanism to invalidate it. CWE-613 (Insufficient Session Expiration) and CWE-347 (Improper Verification of Cryptographic Signature) are both relevant when tokens are issued without meaningful expiry. NIST 800-63B §7.1 requires that session credentials be time-bounded. OWASP A02 covers cryptographic failure modes that include issuing tokens with no or excessive expiry.

## Severity rationale

High because a JWT with no expiry or a multi-year expiry cannot be invalidated after account suspension, role change, or credential compromise — the token remains valid regardless of server-side state.

## Remediation

Set short access token expiry and pair it with rotating refresh tokens rather than issuing long-lived access tokens:

```ts
import { SignJWT } from 'jose'

// Access token: 15 minutes
const accessToken = await new SignJWT({ sub: userId, role: user.role })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('15m')
  .sign(secret)

// Refresh token: 7 days, rotate on each use
const refreshToken = await new SignJWT({ sub: userId, type: 'refresh' })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('7d')
  .sign(secret)
```

For API tokens issued to users (not internal session tokens), store a hash server-side and allow revocation via a token management interface. Never issue tokens with `expiresIn: '100y'` or no expiry.

## Detection

- **ID:** `jwt-reasonable-expiry`
- **Severity:** `high`
- **What to look for:** If the application issues JWTs (JSON Web Tokens) for authentication or API access, examine the token signing configuration. Look for `expiresIn` or `exp` settings in jwt.sign calls, NextAuth JWT configuration, or API token generation code. Check both access tokens and ID tokens. Tokens that never expire (`expiresIn: '100y'` or no expiry set) are a critical concern. Count all instances found and enumerate each.
- **Pass criteria:** JWTs have an expiry set. Access tokens expire within 15 minutes to 1 hour. Longer-lived tokens (session JWTs, refresh tokens) are acceptable up to 7 days with refresh rotation. Tokens with no expiry are not issued. At least 1 implementation must be confirmed.
- **Fail criteria:** JWTs are issued without an expiry (`exp` claim is missing or set to a value years in the future). Access tokens have a lifetime longer than 24 hours without a corresponding refresh mechanism.
- **Skip (N/A) when:** Application does not use JWTs — session-based auth only with no JWT issuance. Signal: no `jsonwebtoken`, `jose`, or JWT-related library, and no custom token signing code.
- **Detail on fail:** `"JWT access tokens issued with expiresIn: '365d' — tokens are valid for a full year and cannot be revoked once issued"` or `"jsonwebtoken.sign() called without expiresIn — tokens have no expiry"`.
- **Remediation:** JWTs are stateless and cannot be individually revoked — the only way to enforce logout or account suspension is through expiry. Short expiry limits the damage window:

  ```ts
  import { SignJWT } from 'jose'

  const accessToken = await new SignJWT({ sub: userId, role: user.role })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m') // Access token: 15 minutes
    .sign(secret)

  const refreshToken = await new SignJWT({ sub: userId, type: 'refresh' })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('7d') // Refresh token: 7 days, rotate on use
    .sign(secret)
  ```

  Use short access token lifetimes paired with refresh token rotation rather than long-lived access tokens.

## External references

- cwe CWE-613
- cwe CWE-347
- owasp A02
- nist SP-800-63B §7.1

Taxons: access-control, cryptography-and-secrets

HTML version: https://auditbuffet.com/patterns/ab-002220
