Intermediate · Authentication and authorization · topic 33 of 54 · 30 min
JSON Web Tokens (JWT)
Explanation
What a JWT looks like
A JWT (JSON Web Token, said "jot") is three pieces of text joined by dots: header.payload.signature. The header says how it was signed. The payload holds claims, which are facts like "user 42, expires at 10:15". The signature proves the server made it and nobody changed it.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 <- header {"alg":"HS256","typ":"JWT"}
.
eyJzdWIiOiI0MiIsIm5hbWUiOiJBc2hhIiwiZXhwIjoxOTAwMDAwMDAwfQ
<- payload {"sub":"42","name":"Asha","exp":1900000000}
.
v-5a3JP0uhw6kTBBLn0jq7yvz6nFx2PctEvhWBfF56A <- signature (HMAC-SHA256)base64url: encoding, not hiding
Each part is base64url encoded. Base64 turns bytes into letters, digits and a few symbols; the "url" version uses - and _ instead of + and / and drops the = padding, so the token is safe in URLs. Encoding is not encryption: anyone can decode the payload in one line.
Common claims
| Claim | Meaning | Example |
|---|---|---|
| sub | Subject: who the token is about (user id) | "42" |
| exp | Expiry time, in seconds since 1 Jan 1970 | 1900000000 |
| iat | Issued at, in seconds | 1899999100 |
| iss | Issuer: who made it | "https://auth.myshop.in" |
| aud | Audience: who it is meant for | "myshop-api" |
Signing: HS256 or RS256
| HS256 | RS256 / ES256 | |
|---|---|---|
| Keys | One shared secret signs and verifies | Private key signs, public key verifies |
| Good for | One backend that both makes and checks tokens | Many services, or tokens from a login provider like Google |
| Risk | Every service with the secret can also create tokens | Only the private key holder can create tokens |
// Example only: runs on a server with Node.js and the jose package
import { SignJWT, jwtVerify } from 'jose';
const secret = new TextEncoder().encode(process.env.JWT_SECRET);
const token = await new SignJWT({ role: 'customer' })
.setProtectedHeader({ alg: 'HS256' })
.setSubject('42')
.setIssuedAt()
.setExpirationTime('15m')
.sign(secret);
// Verify checks the signature AND exp; pin the allowed algorithm
const { payload } = await jwtVerify(token, secret, { algorithms: ['HS256'] });Short life, refresh tokens and storage
Keep access tokens short-lived (5 to 15 minutes). A refresh token is a second, long-lived token used only to get a new access token. Store refresh tokens on the server so you can cancel them, and rotate them (give a new one each time).
In a browser, keep tokens in an HttpOnly, Secure, SameSite cookie rather than localStorage, because any XSS bug can read localStorage. Mobile apps use the phone's secure storage (Keychain or Keystore).
Logging out a JWT
A JWT stays valid until exp, even after the user presses logout, because the server does not look it up. To block one early you need a denylist: store its id (the jti claim) until it would expire, and check that list on each request. In practice: short expiry plus deleting the refresh token.
Watch the lesson
JSON Web Tokens (JWT) · 1/7
A JWT is a signed token that carries facts about the user. Let us open one up.
Prefer a video by a teacher? Find free videos on “jwt authentication explained access token refresh token” on YouTube ↗
In real life: A food delivery app and its API
When you log in on a food delivery app, the backend returns a short-lived access token (a JWT with sub, exp and your role) and a refresh token. Each API call sends the JWT in the Authorization: Bearer header. The order service and the payment service both verify the signature with the public key, without calling the login service. Every 15 minutes the app quietly uses the refresh token to get a new JWT.
Practice (0/2 done)
Write decodePayload(token) that returns the payload object of a JWT: take the middle part, turn base64url into base64 (- becomes +, _ becomes /), decode it with atob, and JSON.parse it. Also write isExpired(payload, nowSeconds) that returns true when the payload has no numeric exp, or when nowSeconds >= exp.
Write async signHS256(payload, secret) and async verifyHS256(token, secret). Sign: header = base64url of JSON.stringify({ alg: "HS256", typ: "JWT" }), body = base64url of JSON.stringify(payload), signature = HMAC-SHA256 of "header.body" with the secret (crypto.subtle.importKey("raw", ..., { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]) and crypto.subtle.sign). Return "header.body.signature". Verify: return the payload object if the signature is right (use crypto.subtle.verify), otherwise null. The base64url helpers are given.
Quick quiz
Read more (free)
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.