JSON Web Tokens

A JWT is three base64url segments joined by dots: a header naming the algorithm, a payload of claims, and a signature over the first two. Nothing is encrypted, so a JWT carries identity, never secrets. Its value is that a service validates one with a key it already has, without a database lookup.

The three segments of a signed JWT
The three segments of a signed JWT
Issuing a token, then five ways verification failsJavaScript
import jwt from 'jsonwebtoken';          // npm i jsonwebtoken@9.0.3
const secret = 'a-32-byte-or-longer-random-secret!!';
const token = jwt.sign({ sub: '4711', role: 'editor' }, secret,
  { algorithm: 'HS256', expiresIn: '15m', issuer: 'api.example.com', audience: 'web' });
const [h, p, s] = token.split('.');
const opts = { algorithms: ['HS256'], issuer: 'api.example.com', audience: 'web' };
const claims = { ...JSON.parse(Buffer.from(p, 'base64url')), role: 'admin' };
const forged = Buffer.from(JSON.stringify(claims)).toString('base64url');
const none = Buffer.from('{"alg":"none","typ":"JWT"}').toString('base64url');
console.log('verify :', jwt.verify(token, secret, opts).role);
for (const [label, tok, key, o] of [
  ['wrong secret  ', token, 'other-secret', opts],
  ['role escalated', `${h}.${forged}.${s}`, secret, opts],
  ['wrong audience', token, secret, { ...opts, audience: 'mobile' }],
  ['expired       ', jwt.sign({ sub: '1' }, secret, { expiresIn: '-1s' }), secret, opts],
  ['alg=none      ', `${none}.${p}.`, secret, opts]]) {
  try { jwt.verify(tok, key, o); console.log(label, 'ACCEPTED - never happens'); }
  catch (e) { console.log(label, e.name + ':', e.message); }
}
Output
verify : editor
wrong secret   JsonWebTokenError: invalid signature
role escalated JsonWebTokenError: invalid signature
wrong audience JsonWebTokenError: jwt audience invalid. expected: mobile
expired        TokenExpiredError: jwt expired
alg=none       JsonWebTokenError: jwt signature is required

The algorithms option is what stops the classic attack. Without it a verifier trusts the alg field in the attacker-supplied header; against an RS256 setup the attacker changes it to HS256 and signs with the public key, which the server then uses as the HMAC secret. Pin the algorithm, check issuer and audience, and treat expiresIn as mandatory. HS256 shares one secret; RS256 and ES256 let many services verify while only the issuer mints.

A signed token stays valid until exp, so you cannot revoke one by deleting a database row. The standard answer is a 5-to-15-minute access token plus a long-lived refresh token stored server-side. When both parties are your own browser and your own server, a signed session cookie is simpler and revocable by default (Express.js). github.com/panva/jose (https://github.com/panva/jose 7,812 ) (6.2.12) is the modern alternative, built on crypto.subtle so it also runs on edge runtimes.