Node.js · APIs & Data

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.

Advanced 6 min Lesson 28 of 37Examples target Node.js 22 LTS.
Before you start:Authentication
0/37 in Node.js
On this page · 6 min · Advanced

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#

JavaScript Illustrative
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#

JavaScriptjwt.js Illustrative
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();
});
Expected output
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#

  1. jwt.sign creates header.payload.signature with a 15-minute expiry.
  2. Decoding the payload shows it is readable — only the signature protects it.
  3. requireJwt checks the Bearer scheme and verifies the signature, algorithm and expiry.
  4. Changing even one character of the token breaks the signature → 401.
  5. 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#

Not pinning algorithms

Always pass algorithms to jwt.verify to block algorithm-confusion attacks.

Long-lived access tokens

A stolen 30-day token works for 30 days. Use minutes-long access tokens plus refresh tokens.

Sensitive data in the payload

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?
jwt.decode(token).exp - jwt.decode(token).iat.
Show solution
JavaScript Illustrative
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#

JWT quizMultiple choice Question 1 of 3

Can anyone read a JWT’s payload?

Score so far: 0/0

Want more? Practise all Node.js MCQs · Node.js interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.