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.

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); }
}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.