A JSON Web Token is two small JSON objects, encoded and signed so that a server can hand a client a proof of identity and later recognize it without a database lookup. JWTs power OAuth and OpenID Connect, API authentication and password-reset links. After this lesson you will be able to read what is inside a token, explain how the signature protects it, issue and verify tokens in Node.js, and avoid the mistakes that make JWT-based systems insecure.
A JWT is three Base64URL-encoded parts separated by dots: header.payload.signature. This is a real token signed with the secret your-256-bit-secret:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cDecoding the first two parts gives ordinary JSON. The header says how the token is signed; the payload carries the claims:
{"alg":"HS256","typ":"JWT"}{"sub":"1234567890","name":"John Doe","iat":1516239022}Base64URL is Base64 with - and _ instead of + and / and no = padding, so a token is safe inside URLs and HTTP headers. The third part is the signature: for HS256 it is HMAC-SHA256(header + "." + payload, secret). Change one character of the payload and the signature no longer matches. Asymmetric algorithms (RS256, ES256) sign with a private key and verify with a public key, so many services can verify tokens only the auth server can issue.
Because the payload is only encoded, anyone can read it:
const [, payload] = token.split(".");
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
// In a browser: JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")))
console.log(claims.sub); // "1234567890"Two consequences follow. Never put secrets in a JWT; the user, browser extensions and anyone who intercepts it can read it. And decoding proves nothing: a server must verify the signature before trusting any claim, otherwise an attacker edits the payload to "role":"admin" and re-encodes it.
The payload is free-form JSON, but RFC 7519 registers a handful of claim names that libraries understand:
| Claim | Meaning | Example |
|-------|---------|---------|
| iss | Issuer, who created the token | "https://auth.example.com" |
| sub | Subject, usually the user ID | "42" |
| aud | Audience, which service may accept it | "inventory-api" |
| exp | Expiration time | 1790000000 |
| nbf | Not valid before | 1789999000 |
| iat | Issued at | 1789999000 |
| jti | Unique token ID, for revocation lists | "c3f1..." |
Times are NumericDate values: whole seconds since the Unix epoch, not the milliseconds Date.now() returns. Add custom claims such as role sparingly; the token travels with every request.
The jose library implements the JOSE standards on Node.js, browsers and edge runtimes; jsonwebtoken is the older Node-only alternative (jwt.sign(), jwt.verify()). The sample at the top of this lesson issues a 15-minute token and verifies it. jwtVerify checks the signature, exp and nbf, and rejects a wrong iss or aud; every failure throws, so a route guard is short:
export async function requireAuth(req, res, next) {
const token = req.headers.authorization?.replace(/^Bearer /, "");
if (!token) return res.status(401).json({ error: "missing token" });
try {
const { payload } = await jwtVerify(token, secret, { algorithms: ["HS256"] });
req.user = { id: payload.sub, role: payload.role };
next();
} catch {
res.status(401).json({ error: "invalid or expired token" });
}
}Clients send the token as Authorization: Bearer <token>. The server needs no session store: any instance holding the key can verify a request.
"alg":"none" or swapped RS256 for HS256 so the public key became the HMAC secret. Always pass an explicit algorithms allowlist.jti denylist for emergencies.localStorage. Any XSS can read it. Prefer httpOnly, Secure, SameSite cookies when the client is a browser.aud and iss. A token for one service must not open another; verify both.What are the three parts of a JWT?
header.payload.signature: two Base64URL-encoded JSON objects plus a signature over them.iss, sub, aud, exp, iat, jti) use seconds since the epoch and are checked by libraries such as jose.Authorization: Bearer <token>; the server verifies without a session store.httpOnly cookies in browsers.Next lesson: JSON Security: Injection, Prototype Pollution and Safe Parsing — the attacks that target JSON handling code and the habits that prevent them.