JSON Web Tokens (JWT): How JSON Carries Identity

Advanced
12 min

JSON Web Tokens (JWT): How JSON Carries Identity

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.

Anatomy of a Token

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:

text
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Decoding the first two parts gives ordinary JSON. The header says how the token is signed; the payload carries the claims:

json
{"alg":"HS256","typ":"JWT"}
json
{"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.

Decoding Is Not Verifying

Because the payload is only encoded, anyone can read it:

javascript
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.

Standard Claims

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.

Issuing and Verifying in Node.js

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:

javascript
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.

Classic Mistakes

  • Letting the token choose the algorithm. Historic attacks used "alg":"none" or swapped RS256 for HS256 so the public key became the HMAC secret. Always pass an explicit algorithms allowlist.
  • Long-lived tokens with no revocation. A stolen 30-day token is a 30-day breach. Use short-lived access tokens (5 to 15 minutes) plus refresh tokens, and keep a jti denylist for emergencies.
  • Storing tokens in localStorage. Any XSS can read it. Prefer httpOnly, Secure, SameSite cookies when the client is a browser.
  • Weak secrets. HS256 needs 256 bits of randomness; a password-like secret can be brute-forced offline from one token.
  • Ignoring aud and iss. A token for one service must not open another; verify both.
Quick Quiz
Question 1 of 3

What are the three parts of a JWT?

Key Takeaways

  • A JWT is header.payload.signature: two Base64URL-encoded JSON objects plus a signature over them.
  • Anyone can decode a token; only verifying the signature with the key proves it is genuine and unmodified.
  • Standard claims (iss, sub, aud, exp, iat, jti) use seconds since the epoch and are checked by libraries such as jose.
  • Send tokens as Authorization: Bearer <token>; the server verifies without a session store.
  • Pin the algorithm, keep tokens short-lived, never store secrets in the payload, and prefer 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.

JSON Web Tokens (JWT): How JSON Carries Identity - JSON | CodeYourCraft | CodeYourCraft