Back to Blog
JWTAuthenticationSecurityNode.js

JWT Authentication: Security Best Practices

Implement secure JWT authentication. Learn token structure, refresh strategies, and security considerations.

B
Bootspring Team
Engineering
February 27, 2026
4 min read

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 claims

JWTs require careful implementation. Use short expiration, secure storage, and proper validation.

Share this article

Help spread the word about Bootspring