# JWT validation covers signature, algorithm, expiry, and audience

- **Pattern:** `ab-002406` (`security-hardening.auth-session.jwt-validation`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002406
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002406)

## Why it matters

JWT algorithm confusion is one of the most exploited vulnerabilities in token-based auth. CWE-347 (Improper Verification of Cryptographic Signature) and CWE-290 (Authentication Bypass by Spoofing) describe how accepting `alg: 'none'` or failing to whitelist algorithms lets an attacker forge arbitrary tokens without the signing key. OWASP A07 (Identification & Authentication Failures) and NIST 800-53 IA-8 require integrity verification of authentication tokens. An RS256 public key misused as an HS256 secret is a textbook attack that compromises every user account on the system. Missing audience validation allows tokens issued for one service to be replayed against another.

## Severity rationale

High because algorithm confusion or missing signature validation lets an attacker mint valid tokens for any user without ever obtaining the secret key.

## Remediation

Use the `jose` library and always specify the exact allowed algorithm. Never allow `'none'`:

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

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

// Signing
const token = await new SignJWT({ userId: user.id })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setExpirationTime('15m')
  .setIssuer('https://yourapp.com')
  .setAudience('https://yourapp.com/api')
  .sign(secret)

// Verification — rejects wrong alg, expired tokens, wrong iss/aud
const { payload } = await jwtVerify(token, secret, {
  algorithms: ['HS256'],
  issuer: 'https://yourapp.com',
  audience: 'https://yourapp.com/api',
})
```

Never pass an untrusted `alg` claim from the token header to your verification function.

## Detection

- **ID:** `jwt-validation`
- **Severity:** `high`
- **What to look for:** List all JWT verification locations in the codebase. For each, examine JWT verification code. Look for the `algorithms` option explicitly set (not left as default accept-all). Verify that `exp` (expiry) is checked. Check whether `aud` (audience) and `iss` (issuer) are validated when present in the token. The most critical failure is accepting algorithm `"none"` or accepting any algorithm without restriction.
- **Pass criteria:** JWT verification explicitly specifies allowed algorithms (never `none`). Expiry is verified. If audience/issuer claims are present in issued tokens, they are validated on receipt — at least 4 validation fields (signature, algorithm, expiry, audience) must be checked at every verification point. Report: "X JWT verification points found, all Y validate signature, algorithm, expiry, and audience."
- **Fail criteria:** JWT verification accepts algorithm `"none"`, or no algorithm whitelist is specified, or expiry is not checked, or `alg: "none"` could be injected via the token header.
- **Skip (N/A) when:** The application does not use JWTs (uses session cookies or managed auth provider tokens).
- **Detail on fail:** Quote the actual JWT verification code showing the missing validation. Example: `"JWT verification does not specify algorithms array — vulnerable to algorithm confusion attack (HS256/RS256 swap)"` or `"JWT expiry (exp claim) not verified — tokens remain valid indefinitely after their stated expiry"`
- **Remediation:** Use the `jose` library (Web Crypto compatible) or `jsonwebtoken` with explicit algorithm configuration:

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

  const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET)

  // Signing
  const token = await new SignJWT({ userId: user.id, role: user.role })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m')
    .setIssuer('https://yourapp.com')
    .setAudience('https://yourapp.com/api')
    .sign(JWT_SECRET)

  // Verification — will reject wrong algorithm, expired tokens, wrong iss/aud
  const { payload } = await jwtVerify(token, JWT_SECRET, {
    algorithms: ['HS256'],  // Only allow HS256, never 'none'
    issuer: 'https://yourapp.com',
    audience: 'https://yourapp.com/api',
  })
  ```

---

## External references

- cwe CWE-347
- cwe CWE-290
- owasp A07
- nist IA-8

Taxons: access-control, cryptography-and-secrets

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