JSON Web Token (JWT) Authentication
Understand JSON Web Tokens (JWT), their structure, and a step-by-step token sign/verify integration guide.
Outline↓
JSON Web Token (JWT) Authentication
Why stateless authentication is used
Historically, web servers used Session-based Authentication. Here is how it worked:
- A user logs in with their credentials.
- The server creates a unique session ID row in a database or Redis cache, and sets it in the user's browser cookie.
- On every subsequent page request, the browser transmits the cookie back.
- The Bottleneck: The server must query the database or cache on every single request to verify the session exists and fetch the user's profile details.
For systems with millions of active users, checking the database for every image click, API request, or page load creates massive database load.
Session-based check:
Browser ==(Session ID)==> Web Server ===(DB Query)===> Database (Slow!)
JWT (JSON Web Token) was created to make authentication stateless, removing the need for database lookups on every request.
Core Specifications
- Why it exists: It was created to provide a self-contained, cryptographically signed format to securely transmit user identity claims between clients and servers without storing state.
- What problem it solves: It solves the performance bottleneck of session-based auth, where the server must query databases or cache pools (e.g., Redis) on every single page interaction or API request to verify active session IDs.
- How it works internally: It concatenates three Base64URL-encoded strings separated by dots: the Header (identifying algorithm and type), the Payload (containing user details and expiration), and the Signature (computed by hashing the header and payload with a private server secret). The server verifies the signature to validate the token.
- When to use it: Use it for stateless REST/GraphQL APIs, cross-domain Single Sign-On (SSO), secure inter-microservice communication, and issuing short-lived expiring links for private file downloads.
- How it is used in real projects: In microservices, the auth gateway signs a JWT with a private key, and downstream services verify the signature locally with a public key, authorizing users instantly without checking a central database.
Mental Model
Think of a JWT as a VIP Hand Stamp at a music festival.
- Traditional Session: The security guard has a written registration checklist. Every time you change stages, they must check their paper list (the database query) to verify your name.
- JWT: When you first buy a ticket, the gatekeeper stamps your hand with a special neon stamp. The stamp contains text: "Admit One, User ID 42, Expires 9 PM", and is sealed with a unique chemical signature (the server's secret key).
- Every time you enter a stage, the guard shines a UV light on your hand (cryptographic signature verification). If the chemical signature matches, you walk in!
- The guard never checks a checklist. The stamp itself is the proof.
Codebase Integration
Here is how to set up, sign, and verify JWTs inside a TypeScript backend application.
Step 1: Install Dependencies
Install the token signature library and its TypeScript types.
# In your backend project directory
npm install jsonwebtoken
npm install @types/jsonwebtoken --save-dev
Step 2: Configure Environment Secret
Open your .env file and define a secure signing password. The server uses this key to hash your tokens.
# .env (Environment config)
JWT_SECRET="my_super_secure_private_signing_key_98765"
Step 3: Implement Signing and Verification Utilities
Create a helper utility in src/lib/jwt.ts to sign and verify tokens.
// filepath: src/lib/jwt.ts
// Purpose: Provide clean helper methods to issue and validate JWT payloads.
import jwt from 'jsonwebtoken';
const SECRET = process.env.JWT_SECRET || 'fallback-secret-for-dev';
export interface TokenPayload {
userId: string;
email: string;
role: string;
}
// Create a token that expires in 24 hours
export function generateToken(payload: TokenPayload): string {
return jwt.sign(payload, SECRET, { expiresIn: '24h' });
}
// Verify and decode a token
export function verifyToken(token: string): TokenPayload | null {
try {
const decoded = jwt.verify(token, SECRET) as TokenPayload;
return decoded;
} catch (error) {
// Returns null if token is expired, tampered, or invalid
return null;
}
}
Step 4: Configure Token Middleware Check
Create an authentication middleware. Put this inside src/middlewares/auth.ts to secure endpoints.
// filepath: src/middlewares/auth.ts
// Purpose: Express middleware verifying JWT header values.
import { Request, Response, NextFunction } from 'express';
import { verifyToken, TokenPayload } from '../lib/jwt';
// Extend Express Request typing to include decoded user
export interface AuthenticatedRequest extends Request {
user?: TokenPayload;
}
export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Access token required. Format: Bearer <token>' });
}
const token = authHeader.split(' ')[1];
const user = verifyToken(token);
if (!user) {
return res.status(403).json({ error: 'Session expired or token modified' });
}
req.user = user; // Attach payload to request
next();
}
Step 5: Secure Database API Endpoint
Apply the middleware to route controllers.
// filepath: src/routes/userRoutes.ts
// Purpose: Routing definitions for user profile retrieval, protected by requireAuth.
import { Router } from 'express';
import { requireAuth, AuthenticatedRequest } from '../middlewares/auth';
const router = Router();
router.get('/profile', requireAuth, (req: AuthenticatedRequest, res) => {
// Read details directly from the decrypted JWT payload (No database query needed!)
const userId = req.user?.userId;
const email = req.user?.email;
res.json({
message: 'Authorized access granted',
profile: { id: userId, email }
});
});
export default router;
Practical Project Use Cases
1. Decoupled Single Sign-On (SSO) across Microservices
In a distributed microservice network (e.g. an e-commerce platform split into a billing server, checkout server, and inventory service), logging users in without a shared session database is critical.
- Real-World Example: The central login service signs a JWT using a Private RSA/ECDSA Key. Other microservices load the matching Public Key to decrypt tokens. This lets the checkout and billing microservices authenticate users independently, without making requests back to the main user database:
// filepath: src/services/billingService.ts
// Purpose: Authenticate billing calls using public key signature checks.
import jwt from 'jsonwebtoken';
import fs from 'fs';
// Load the public key distributed to this service
const publicKey = fs.readFileSync('./keys/public.key', 'utf8');
export function checkBillingScope(token: string) {
try {
// Verify using asymmetric encryption (rs256 algorithm)
const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] }) as any;
return { valid: true, userId: decoded.sub, scope: decoded.scope };
} catch (err) {
return { valid: false, error: "Invalid token certificate" };
}
}
2. Expiring Link URLs for Private Cloud Downloads
Preventing users from sharing links to private file directories (e.g. premium PDF sheets stored on AWS S3).
- Real-World Example: We issue a JWT containing the target filename, setting the expiry (
exp) to 5 minutes. We append this token as a query parameter?token=...to the file URL. When the download route is hit, our storage server parses the token. If a user shares the link, it expires and becomes invalid within 5 minutes.
Common Pitfalls
1. Storing Sensitive Data in the Payload
A JWT is not encrypted by default; it is merely base64 encoded. Anyone can decode a JWT payload in seconds using tools like jwt.io.
- Fix: Never store passwords, phone numbers, or credit card details in the JWT.
2. Storing JWTs in LocalStorage
Saving tokens in localStorage makes them accessible to JavaScript. If your site has a Cross-Site Scripting (XSS) vulnerability, an attacker can steal your JWT with: localStorage.getItem('token') and hijack the user session.
- Fix: Store JWTs in an HTTP-only, Secure, SameSite=Strict Cookie. This hides the token from JavaScript access.
Interview Questions
What are the three parts of a JWT?
A JWT is composed of three parts separated by periods (.):
- Header: Specifies the token type (JWT) and the hashing algorithm used (e.g. HS256).
- Payload: Contains the statements (claims) about the user, such as user ID, role, and expiration timestamp.
- Signature: Formed by encoding the header and payload, joining them with a period, and hashing them using a private server secret.
Summary
- Stateless: Authentic state is stored entirely in the token string itself, requiring no server table checks.
- Security: Signature validation checks protect tokens against client tampering.
- Storage: Prefer HTTP-only cookies over
localStorageto protect against token theft via XSS.