Claude Fable 5.1 & GPT-6 Astra packages are live

Jwt

Free

Issuing and validating JSON Web Tokens safely — algorithm pinning, claim validation, key rotation, and why revocation is the hard part.

214 lines8.8 KB Glm Security
targetModels
GLM-5.3GLM-5.2GLM-5 FamilyGLM-4.6Future GLM Models
name
jwt
category
Security
description
Issuing and validating JSON Web Tokens safely — algorithm pinning, claim validation, key rotation, and why revocation is the hard part.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for GLM: scripts/model-profiles.json -->

#Task boundary

  1. Implement only what the task names; no extra abstractions or files.
  2. English-only comments and identifiers.
  3. Stop when the checklist passes.

#Purpose

Rules for using JWTs without introducing the failure modes the format invites.

First, decide whether you need one. A JWT is the right tool when a resource server must validate a token without calling the issuer. If your API can reach a session store, an opaque session identifier is simpler, revocable instantly, and has none of the pitfalls below. Reach for a JWT because you need stateless verification — not because it is the default.


#Algorithm — the classic break

#Pin the algorithm at verification. Never read it from the token.

The alg header is attacker-controlled. A verifier that trusts it can be defeated two ways:

alg: none — the token declares it is unsigned. A verifier honouring this accepts a forged payload with an empty signature.

RS256HS256 confusion — the attacker changes the header to HS256 and signs with the public RSA key as the HMAC secret. A verifier that selects the algorithm from the header will validate it, because the public key is not secret.

js
// WRONG — algorithm comes from the token
jwt.verify(token, key);

// RIGHT — algorithm is fixed by the server
jwt.verify(token, publicKey, {
  algorithms: ["RS256"],          // explicit allow-list
  issuer: "https://auth.example.com",
  audience: "https://api.example.com",
});

Always pass an explicit algorithms allow-list containing exactly the algorithm you issue. Never include none. Never let the list vary with the token.

AlgorithmUse
EdDSA (Ed25519)Preferred asymmetric; small, fast, no curve pitfalls
RS256 / PS256Asymmetric, widely supported; PS256 preferred over RS256
ES256Asymmetric; verify the library rejects malleable signatures
HS256Symmetric — only when issuer and verifier are the same service
noneNever

Use HS256 only where a single service both signs and verifies. The moment a second party must verify, they need the secret, and then they can also mint tokens.


#Claim validation

Verifying the signature proves integrity. It does not prove the token is for you, current, or from whom you expect. Validate every claim explicitly.

ClaimMeaningRule
expExpiryRequired. Reject if past. Allow ≤ 60 s clock skew.
nbfNot beforeReject if in the future
iatIssued atUse to enforce a maximum token age
issIssuerMust equal your expected issuer exactly
audAudienceMust contain this service. Prevents token reuse across APIs.
subSubjectThe user identity; never trust a custom user_id instead
jtiToken IDNeeded for replay detection and revocation lists

Never skip aud validation in a multi-service estate. A token minted for the analytics API is otherwise accepted by the payments API.

js
// Validate claims explicitly. A verified signature says the token was not
// tampered with — not that it was minted for this service, or is still current.
const { payload } = await jwtVerify(token, keySet, {
  algorithms: ["EdDSA"],
  issuer: "https://auth.example.com",
  audience: "https://api.example.com",
  clockTolerance: 60,               // seconds
  maxTokenAge: "15m",               // bounds `iat`, not just `exp`
});

Never trust unvalidated custom claims for authorisation — {"role":"admin"} in a token you did not verify the issuer of is just attacker input.

Keep expiry short: 5–15 minutes for access tokens. Long-lived access tokens are the reason revocation becomes an unsolvable problem.


#Keys

  1. Store signing keys in a secret manager or KMS, never in the repository, never in a client bundle. See Security/secret-management.
  2. HMAC secrets must be ≥ 256 bits of CSPRNG output. A guessable secret makes the signature decorative; HS256 with a dictionary word is brute-forced offline.
  3. Publish public keys via JWKS (/.well-known/jwks.json) and select the key by the token's kid.
  4. Rotate on a schedule. Publish the new key before signing with it, and keep the old key verifiable until every issued token has expired.
  5. Cache JWKS, but bound the cache and re-fetch on unknown kid. Never fetch a key from a URL inside the token — that is a server-side request forgery and key-injection vector in one.

#Revocation — the honest part

A JWT is valid until it expires. That is the whole point of stateless verification, and it is also the problem: you cannot un-issue one.

Practical approaches, in increasing cost:

  1. Short expiry + refresh tokens. Access tokens live minutes; the refresh token is opaque, stored server-side, and revocable. This is the standard design and the one to reach for first.
  2. Deny-list by jti until natural expiry. Requires a shared store — you have reintroduced state, but only for revoked tokens, and entries expire.
  3. Token version per user. Store tokenVersion on the user; include it as a claim; reject on mismatch. One row read per request, invalidates every token for that user at once. Good for "log out everywhere" and forced password reset.
js
// Token-version revocation: one indexed read per request, and a single
// increment logs the user out everywhere — on password reset or reported theft.
const user = await db.user.findUnique({
  where: { id: payload.sub },
  select: { tokenVersion: true },
});
if (!user || user.tokenVersion !== payload.ver) {
  throw new Error("token revoked");
}

Never claim tokens are revoked because the client deleted them. Deleting a token client-side is a UI gesture, not a security control.

Refresh tokens must be rotated on use, and reuse of a consumed refresh token must revoke the whole family — that is the signal a token was stolen.


#Transport and storage

  1. Send as Authorization: Bearer <token> over HTTPS only.
  2. In browsers, prefer an HttpOnly; Secure; SameSite cookie over localStorage. A token in localStorage is readable by any script, so any XSS becomes account takeover. If you use cookies, you must handle CSRF — see Security/csrf.
  3. Never put a JWT in a URL. It lands in access logs, Referer headers and browser history.
  4. The payload is base64url, not encrypted. Anyone holding the token can read every claim. Put no secrets, PII, or internal identifiers in it. If confidentiality is required, use JWE — or better, an opaque token.

#Anti-patterns

Anti-patternWhy it failsFix
jwt.verify(token, key) without algorithmsalg: none and RS256→HS256 confusionExplicit allow-list
jwt.decode() used to authenticateDecodes without verifying the signaturejwt.verify()
Skipping audToken from one service accepted by anotherValidate audience
30-day access tokensNothing can be revoked in time5–15 min + refresh
Token in localStorageAny XSS becomes account takeoverHttpOnly cookie
Secrets or PII in the payloadBase64url is encoding, not encryptionKeep claims minimal
Fetching the key from a URL in the tokenAttacker chooses the verifying keyJWKS + kid allow-list
Refresh token reused silentlyTheft goes undetectedRotate on use; revoke family on reuse

#Checklist

  • An opaque session was considered and stateless verification is genuinely needed
  • Verification passes an explicit algorithms allow-list; none never appears
  • HS256 used only where signer and verifier are the same service
  • exp, iss and aud validated on every request; skew ≤ 60 s
  • jwt.decode() is never used as an authentication step
  • Access tokens expire in 5–15 minutes
  • Signing keys come from a KMS or secret manager; HMAC secrets ≥ 256 bits
  • Public keys published via JWKS and selected by kid, never by token URL
  • Key rotation publishes before signing and keeps old keys verifiable
  • A revocation strategy exists — deny-list, token version, or short expiry
  • Refresh tokens rotate on use; reuse revokes the family
  • Tokens never appear in URLs; payload contains no secrets or PII