JWTs provide stateless authentication when implemented securely.
Token Structure#
Header.Payload.Signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Creating Tokens#
1import jwt from 'jsonwebtoken';
2
3interface TokenPayload {
4 sub: string; // Subject (user ID)
5 email: string;
6 role: string;
7}
8
9const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!;
10const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!;
11
12function generateAccessToken(user: User): string {
13 const payload: TokenPayload = {
14 sub: user.id,
15 email: user.email,
16 role: user.role,
17 };
18
19 return jwt.sign(payload, ACCESS_TOKEN_SECRET, {
20 expiresIn: '15m',
21 issuer: 'myapp',
22 audience: 'myapp-users',
23 });
24}
25
26function generateRefreshToken(user: User): string {
27 return jwt.sign(
28 { sub: user.id, tokenVersion: user.tokenVersion },
29 REFRESH_TOKEN_SECRET,
30 { expiresIn: '7d' }
31 );
32}Verifying Tokens#
1function verifyAccessToken(token: string): TokenPayload {
2 try {
3 return jwt.verify(token, ACCESS_TOKEN_SECRET, {
4 issuer: 'myapp',
5 audience: 'myapp-users',
6 }) as TokenPayload;
7 } catch (error) {
8 if (error instanceof jwt.TokenExpiredError) {
9 throw new AuthError('Token expired', 'TOKEN_EXPIRED');
10 }
11 if (error instanceof jwt.JsonWebTokenError) {
12 throw new AuthError('Invalid token', 'INVALID_TOKEN');
13 }
14 throw error;
15 }
16}
17
18// Middleware
19function authMiddleware(req: Request, res: Response, next: NextFunction) {
20 const authHeader = req.headers.authorization;
21
22 if (!authHeader?.startsWith('Bearer ')) {
23 return res.status(401).json({ error: 'Missing token' });
24 }
25
26 const token = authHeader.slice(7);
27
28 try {
29 const payload = verifyAccessToken(token);
30 req.user = payload;
31 next();
32 } catch (error) {
33 if (error.code === 'TOKEN_EXPIRED') {
34 return res.status(401).json({ error: 'Token expired' });
35 }
36 return res.status(401).json({ error: 'Invalid token' });
37 }
38}Refresh Token Flow#
1// Store refresh tokens securely
2async function login(email: string, password: string) {
3 const user = await validateCredentials(email, password);
4
5 const accessToken = generateAccessToken(user);
6 const refreshToken = generateRefreshToken(user);
7
8 // Store refresh token hash in database
9 await db.refreshTokens.create({
10 data: {
11 userId: user.id,
12 tokenHash: hashToken(refreshToken),
13 expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
14 },
15 });
16
17 return { accessToken, refreshToken };
18}
19
20async function refreshTokens(refreshToken: string) {
21 // Verify token
22 const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET) as {
23 sub: string;
24 tokenVersion: number;
25 };
26
27 // Check if token exists in database
28 const storedToken = await db.refreshTokens.findFirst({
29 where: {
30 userId: payload.sub,
31 tokenHash: hashToken(refreshToken),
32 expiresAt: { gt: new Date() },
33 },
34 });
35
36 if (!storedToken) {
37 throw new AuthError('Invalid refresh token');
38 }
39
40 const user = await db.users.findUnique({ where: { id: payload.sub } });
41
42 // Check token version (for invalidation)
43 if (user.tokenVersion !== payload.tokenVersion) {
44 throw new AuthError('Token revoked');
45 }
46
47 // Rotate refresh token
48 await db.refreshTokens.delete({ where: { id: storedToken.id } });
49
50 const newAccessToken = generateAccessToken(user);
51 const newRefreshToken = generateRefreshToken(user);
52
53 await db.refreshTokens.create({
54 data: {
55 userId: user.id,
56 tokenHash: hashToken(newRefreshToken),
57 expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
58 },
59 });
60
61 return { accessToken: newAccessToken, refreshToken: newRefreshToken };
62}Token Revocation#
1// Revoke all tokens for a user
2async function revokeAllTokens(userId: string) {
3 // Increment token version
4 await db.users.update({
5 where: { id: userId },
6 data: { tokenVersion: { increment: 1 } },
7 });
8
9 // Delete all refresh tokens
10 await db.refreshTokens.deleteMany({ where: { userId } });
11}
12
13// Revoke single refresh token
14async function logout(refreshToken: string) {
15 await db.refreshTokens.deleteMany({
16 where: { tokenHash: hashToken(refreshToken) },
17 });
18}HTTP-Only Cookies#
1// Set tokens in cookies (more secure than localStorage)
2function setAuthCookies(res: Response, tokens: AuthTokens) {
3 res.cookie('accessToken', tokens.accessToken, {
4 httpOnly: true,
5 secure: process.env.NODE_ENV === 'production',
6 sameSite: 'strict',
7 maxAge: 15 * 60 * 1000, // 15 minutes
8 });
9
10 res.cookie('refreshToken', tokens.refreshToken, {
11 httpOnly: true,
12 secure: process.env.NODE_ENV === 'production',
13 sameSite: 'strict',
14 path: '/api/auth/refresh', // Only sent to refresh endpoint
15 maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
16 });
17}
18
19function clearAuthCookies(res: Response) {
20 res.clearCookie('accessToken');
21 res.clearCookie('refreshToken', { path: '/api/auth/refresh' });
22}Security Checklist#
1// 1. Use strong secrets
2const secret = crypto.randomBytes(64).toString('hex');
3
4// 2. Set appropriate expiration
5jwt.sign(payload, secret, { expiresIn: '15m' }); // Short-lived
6
7// 3. Validate all claims
8jwt.verify(token, secret, {
9 issuer: 'myapp',
10 audience: 'myapp-users',
11 algorithms: ['HS256'], // Prevent algorithm confusion
12});
13
14// 4. Use asymmetric keys for distributed systems
15const privateKey = fs.readFileSync('private.pem');
16const publicKey = fs.readFileSync('public.pem');
17
18jwt.sign(payload, privateKey, { algorithm: 'RS256' });
19jwt.verify(token, publicKey, { algorithms: ['RS256'] });
20
21// 5. Never store sensitive data in payload
22// ❌ Don't include: passwords, SSN, credit cards
23// ✅ Include: user ID, role, minimal claimsJWTs require careful implementation. Use short expiration, secure storage, and proper validation.