Topic 7 of 55
JWT Authentication
Overview
JWT (JSON Web Token) is the standard for stateless authentication in REST APIs. Unlike sessions (which store state on the server), JWTs are self-contained tokens that include user data — perfect for scalable, stateless APIs and mobile apps.
Syntax
javascript
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
// Creating a JWT
const payload = { sub: user.id, email: user.email, role: user.role };
const token = jwt.sign(
payload,
process.env.JWT_SECRET,
{ expiresIn: '7d' } // token expires in 7 days
);
// Verifying a JWT
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// decoded.sub = user ID, decoded.role = user role
} catch (error) {
if (error.name === 'TokenExpiredError') { /* token expired */ }
if (error.name === 'JsonWebTokenError') { /* invalid token */ }
}
// Password hashing with bcrypt
const SALT_ROUNDS = 12;
const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);
const isMatch = await bcrypt.compare(plainPassword, hashedPassword);Common Pitfalls
- Never store sensitive data (passwords, SSN, card numbers) in JWT payload — the payload is base64-encoded, not encrypted.
- Access tokens should be short-lived (15 min); refresh tokens long-lived (7-30 days). Store refresh tokens server-side to enable revocation.
- Interview tip: JWTs can't be 'invalidated' before expiry (stateless) — this is why short access token expiry + refresh token rotation matters.
Real-World Example
Complete auth system with refresh tokens:
example
javascript
// auth.service.ts
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { prisma } from './db';
const ACCESS_EXPIRY = '15m'; // short-lived
const REFRESH_EXPIRY = '30d'; // long-lived
function generateTokens(userId: string) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
process.env.JWT_SECRET!,
{ expiresIn: ACCESS_EXPIRY }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: REFRESH_EXPIRY }
);
return { accessToken, refreshToken };
}
export async function login(email: string, password: string) {
const user = await prisma.user.findUnique({ where: { email } });
if (!user) throw new UnauthorizedError('Invalid credentials');
const isValid = await bcrypt.compare(password, user.passwordHash);
if (!isValid) throw new UnauthorizedError('Invalid credentials');
const tokens = generateTokens(user.id);
// Store refresh token hash in DB (can be revoked!)
await prisma.refreshToken.create({
data: { userId: user.id, tokenHash: await bcrypt.hash(tokens.refreshToken, 8) },
});
return { user: { id: user.id, email, name: user.name }, ...tokens };
}
export async function refreshAccessToken(refreshToken: string) {
const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as any;
if (decoded.type !== 'refresh') throw new UnauthorizedError('Invalid token type');
return generateTokens(decoded.sub);
}