JWT
A JWT is a signed note that says “this is user 7, an admin, valid for 15 minutes”. Any server with the key can check the signature — no session lookup needed.
On this page · 6 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Explain the three parts of a JWT
- Sign tokens with claims and an expiry
- Verify Bearer tokens in middleware
- Know the security trade-offs and storage options
Explanation#
A JWT is header.payload.signature, each base64url-encoded. The payload holds claims (sub = user id, role, exp = expiry). It is signed, not encrypted — anyone can read it, but nobody can change it without the key.
Sign with jwt.sign({ sub: user.id, role }, SECRET, { expiresIn: "15m" }). Use a long random secret from the environment (or an RSA/EC key pair).
Clients send Authorization: Bearer <token>. Middleware calls jwt.verify(token, SECRET, { algorithms: ["HS256"] }); invalid or expired tokens throw → 401.
Keep access tokens short-lived. For longer sessions, issue a refresh token (stored in an httpOnly cookie) to get new access tokens; revoke refresh tokens on logout.
Never put secrets or personal data in the payload. Storing tokens in localStorage exposes them to XSS; httpOnly cookies avoid that but need CSRF protection.
Syntax#
import jwt from 'jsonwebtoken';
const token = jwt.sign({ sub: 7, role: 'admin' }, process.env.JWT_SECRET, { expiresIn: '15m' });
const claims = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
Code example: Issue and verify access tokens#
import express from 'express';
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET ?? 'dev-only-secret-change-me-32-chars-long';
function requireJwt(req, res, next) {
const [scheme, token] = (req.get('authorization') ?? '').split(' ');
if (scheme !== 'Bearer' || !token) return res.status(401).json({ error: 'Missing token' });
try {
req.user = jwt.verify(token, SECRET, { algorithms: ['HS256'] });
next();
} catch (err) {
res.status(401).json({ error: err.name === 'TokenExpiredError' ? 'Token expired' : 'Invalid token' });
}
}
const app = express();
app.use(express.json());
app.post('/login', (req, res) => {
// (password check omitted — see the Authentication lesson)
const token = jwt.sign({ sub: '7', name: 'Asha', role: 'admin' }, SECRET, { expiresIn: '15m' });
res.json({ accessToken: token, expiresIn: 900 });
});
app.get('/api/profile', requireJwt, (req, res) => res.json({ id: req.user.sub, name: req.user.name, role: req.user.role }));
const server = app.listen(3022, async () => {
const base = 'http://localhost:3022';
const { accessToken } = await (await fetch(base + '/login', { method: 'POST' })).json();
const [header, payload] = accessToken.split('.');
console.log('parts:', accessToken.split('.').length);
console.log('header:', Buffer.from(header, 'base64url').toString());
console.log('payload readable by anyone:', JSON.parse(Buffer.from(payload, 'base64url')).name);
const ok = await fetch(base + '/api/profile', { headers: { authorization: `Bearer ${accessToken}` } });
console.log(ok.status, await ok.text());
const tampered = accessToken.slice(0, -2) + (accessToken.endsWith('AA') ? 'BB' : 'AA');
const bad = await fetch(base + '/api/profile', { headers: { authorization: `Bearer ${tampered}` } });
console.log(bad.status, await bad.text());
const expired = jwt.sign({ sub: '7' }, SECRET, { expiresIn: -10 });
const old = await fetch(base + '/api/profile', { headers: { authorization: `Bearer ${expired}` } });
console.log(old.status, await old.text());
server.close();
});
parts: 3
header: {"alg":"HS256","typ":"JWT"}
payload readable by anyone: Asha
200 {"id":"7","name":"Asha","role":"admin"}
401 {"error":"Invalid token"}
401 {"error":"Token expired"}This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- jwt.sign creates header.payload.signature with a 15-minute expiry.
- Decoding the payload shows it is readable — only the signature protects it.
- requireJwt checks the Bearer scheme and verifies the signature, algorithm and expiry.
- Changing even one character of the token breaks the signature → 401.
- An expired token is rejected with a specific message.
Real-world use case#
A mobile app talks to several Node.js microservices. Each service verifies the same JWT with the shared public key, so no service needs to call the auth service on every request.
Common mistakes#
Always pass algorithms to jwt.verify to block algorithm-confusion attacks.
A stolen 30-day token works for 30 days. Use minutes-long access tokens plus refresh tokens.
JWTs are only base64-encoded. Never include passwords, card numbers or secrets.
Best practices#
- Long random secret or asymmetric keys from env
- Short expiry; refresh tokens for long sessions
- Pin algorithms on verify
- Minimal claims (sub, role)
Remember this#
JWT = header.payload.signature — signed, not encrypted.
Authorization: Bearer <token>.
Short-lived access token + refresh token.
Practice exercise#
Your task: Write a function issueToken(user) that signs { sub: user.id, role: user.role } for 10 minutes, and decode its exp to print how many seconds it is valid.
Need a hint?
Show solution
import jwt from 'jsonwebtoken';
const SECRET = 'dev-only-secret-change-me-32-chars-long';
const issueToken = user => jwt.sign({ sub: String(user.id), role: user.role }, SECRET, { expiresIn: '10m' });
const token = issueToken({ id: 7, role: 'customer' });
const { iat, exp } = jwt.decode(token);
console.log('valid for', exp - iat, 'seconds'); // valid for 600 seconds
Mini quiz#
Can anyone read a JWT’s payload?
Want more? Practise all Node.js MCQs · Node.js interview questions
My notes#
Saved in this browser.