Back to Blog
JWTAuthenticationSecurityNode.js

JWT Authentication: Implementation Guide

Implement JWT authentication securely. From token creation to validation to refresh token rotation.

B
Bootspring Team
Engineering
November 20, 2022
6 min read

JSON Web Tokens (JWTs) enable stateless authentication. Here's how to implement them securely.

JWT Structure#

Header.Payload.Signature Header: { "alg": "HS256", "typ": "JWT" } Payload: { "sub": "user123", "iat": 1704067200, "exp": 1704070800, "role": "user" } Signature: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )

Token Creation#

1import jwt from 'jsonwebtoken'; 2 3interface TokenPayload { 4 sub: string; 5 role: string; 6 email: string; 7} 8 9const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET!; 10const REFRESH_TOKEN_SECRET = process.env.REFRESH_TOKEN_SECRET!; 11 12function createAccessToken(user: User): string { 13 const payload: TokenPayload = { 14 sub: user.id, 15 role: user.role, 16 email: user.email, 17 }; 18 19 return jwt.sign(payload, ACCESS_TOKEN_SECRET, { 20 expiresIn: '15m', 21 issuer: 'your-app', 22 audience: 'your-app', 23 }); 24} 25 26function createRefreshToken(user: User): string { 27 return jwt.sign( 28 { sub: user.id, tokenVersion: user.tokenVersion }, 29 REFRESH_TOKEN_SECRET, 30 { 31 expiresIn: '7d', 32 issuer: 'your-app', 33 } 34 ); 35} 36 37// Token pair 38function createTokenPair(user: User): TokenPair { 39 return { 40 accessToken: createAccessToken(user), 41 refreshToken: createRefreshToken(user), 42 }; 43}

Token Validation#

1function verifyAccessToken(token: string): TokenPayload { 2 try { 3 return jwt.verify(token, ACCESS_TOKEN_SECRET, { 4 issuer: 'your-app', 5 audience: 'your-app', 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// Authentication middleware 19async function authenticate( 20 req: Request, 21 res: Response, 22 next: NextFunction 23): Promise<void> { 24 const authHeader = req.headers.authorization; 25 26 if (!authHeader?.startsWith('Bearer ')) { 27 res.status(401).json({ error: 'No token provided' }); 28 return; 29 } 30 31 const token = authHeader.slice(7); 32 33 try { 34 const payload = verifyAccessToken(token); 35 36 // Optionally fetch user from database 37 const user = await prisma.user.findUnique({ 38 where: { id: payload.sub }, 39 select: { id: true, role: true, email: true }, 40 }); 41 42 if (!user) { 43 res.status(401).json({ error: 'User not found' }); 44 return; 45 } 46 47 req.user = user; 48 next(); 49 } catch (error) { 50 if (error instanceof AuthError) { 51 res.status(401).json({ error: error.message, code: error.code }); 52 return; 53 } 54 res.status(401).json({ error: 'Invalid token' }); 55 } 56}

Refresh Token Flow#

1// Store refresh tokens in database 2model RefreshToken { 3 id String @id @default(cuid()) 4 token String @unique 5 userId String 6 user User @relation(fields: [userId], references: [id]) 7 expiresAt DateTime 8 createdAt DateTime @default(now()) 9 revokedAt DateTime? 10 11 @@index([userId]) 12 @@index([token]) 13} 14 15// Refresh endpoint 16app.post('/auth/refresh', async (req, res) => { 17 const { refreshToken } = req.body; 18 19 if (!refreshToken) { 20 return res.status(400).json({ error: 'Refresh token required' }); 21 } 22 23 try { 24 // Verify token signature 25 const payload = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET) as { 26 sub: string; 27 }; 28 29 // Check token in database 30 const storedToken = await prisma.refreshToken.findUnique({ 31 where: { token: refreshToken }, 32 include: { user: true }, 33 }); 34 35 if (!storedToken || storedToken.revokedAt || storedToken.expiresAt < new Date()) { 36 return res.status(401).json({ error: 'Invalid refresh token' }); 37 } 38 39 // Rotate refresh token 40 await prisma.refreshToken.update({ 41 where: { id: storedToken.id }, 42 data: { revokedAt: new Date() }, 43 }); 44 45 // Create new tokens 46 const newRefreshToken = createRefreshToken(storedToken.user); 47 48 await prisma.refreshToken.create({ 49 data: { 50 token: newRefreshToken, 51 userId: storedToken.userId, 52 expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), 53 }, 54 }); 55 56 res.json({ 57 accessToken: createAccessToken(storedToken.user), 58 refreshToken: newRefreshToken, 59 }); 60 } catch (error) { 61 res.status(401).json({ error: 'Invalid refresh token' }); 62 } 63});

Token Revocation#

1// Logout - revoke refresh token 2app.post('/auth/logout', authenticate, async (req, res) => { 3 const { refreshToken } = req.body; 4 5 if (refreshToken) { 6 await prisma.refreshToken.updateMany({ 7 where: { 8 token: refreshToken, 9 userId: req.user.id, 10 }, 11 data: { revokedAt: new Date() }, 12 }); 13 } 14 15 res.status(204).send(); 16}); 17 18// Logout from all devices 19app.post('/auth/logout-all', authenticate, async (req, res) => { 20 await prisma.refreshToken.updateMany({ 21 where: { 22 userId: req.user.id, 23 revokedAt: null, 24 }, 25 data: { revokedAt: new Date() }, 26 }); 27 28 res.status(204).send(); 29}); 30 31// Password change - invalidate all tokens 32async function changePassword(userId: string, newPassword: string): Promise<void> { 33 const hashedPassword = await bcrypt.hash(newPassword, 12); 34 35 await prisma.$transaction([ 36 prisma.user.update({ 37 where: { id: userId }, 38 data: { password: hashedPassword }, 39 }), 40 prisma.refreshToken.updateMany({ 41 where: { userId, revokedAt: null }, 42 data: { revokedAt: new Date() }, 43 }), 44 ]); 45}
1// More secure for web applications 2const COOKIE_OPTIONS: CookieOptions = { 3 httpOnly: true, 4 secure: process.env.NODE_ENV === 'production', 5 sameSite: 'strict', 6 path: '/', 7}; 8 9app.post('/auth/login', async (req, res) => { 10 // Validate credentials... 11 const user = await validateCredentials(req.body); 12 13 const tokens = createTokenPair(user); 14 15 // Store refresh token in HTTP-only cookie 16 res.cookie('refreshToken', tokens.refreshToken, { 17 ...COOKIE_OPTIONS, 18 maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days 19 }); 20 21 // Access token in response body 22 res.json({ accessToken: tokens.accessToken }); 23}); 24 25// Refresh using cookie 26app.post('/auth/refresh', async (req, res) => { 27 const refreshToken = req.cookies.refreshToken; 28 29 if (!refreshToken) { 30 return res.status(401).json({ error: 'No refresh token' }); 31 } 32 33 // Validate and rotate... 34 const tokens = await rotateTokens(refreshToken); 35 36 res.cookie('refreshToken', tokens.refreshToken, { 37 ...COOKIE_OPTIONS, 38 maxAge: 7 * 24 * 60 * 60 * 1000, 39 }); 40 41 res.json({ accessToken: tokens.accessToken }); 42}); 43 44// Logout - clear cookie 45app.post('/auth/logout', (req, res) => { 46 res.clearCookie('refreshToken', COOKIE_OPTIONS); 47 res.status(204).send(); 48});

Client-Side Token Management#

1// Token storage and refresh 2class AuthClient { 3 private accessToken: string | null = null; 4 private refreshPromise: Promise<string> | null = null; 5 6 async getAccessToken(): Promise<string> { 7 if (this.accessToken && !this.isTokenExpired(this.accessToken)) { 8 return this.accessToken; 9 } 10 11 return this.refresh(); 12 } 13 14 private async refresh(): Promise<string> { 15 // Prevent multiple simultaneous refresh requests 16 if (this.refreshPromise) { 17 return this.refreshPromise; 18 } 19 20 this.refreshPromise = this.doRefresh(); 21 22 try { 23 return await this.refreshPromise; 24 } finally { 25 this.refreshPromise = null; 26 } 27 } 28 29 private async doRefresh(): Promise<string> { 30 const response = await fetch('/auth/refresh', { 31 method: 'POST', 32 credentials: 'include', // Include cookies 33 }); 34 35 if (!response.ok) { 36 this.accessToken = null; 37 throw new Error('Refresh failed'); 38 } 39 40 const { accessToken } = await response.json(); 41 this.accessToken = accessToken; 42 return accessToken; 43 } 44 45 private isTokenExpired(token: string): boolean { 46 try { 47 const payload = JSON.parse(atob(token.split('.')[1])); 48 // Add 30 second buffer 49 return payload.exp * 1000 < Date.now() + 30000; 50 } catch { 51 return true; 52 } 53 } 54} 55 56// Axios interceptor 57axios.interceptors.request.use(async (config) => { 58 const token = await authClient.getAccessToken(); 59 config.headers.Authorization = `Bearer ${token}`; 60 return config; 61});

Security Best Practices#

Tokens: ✓ Short-lived access tokens (15 minutes) ✓ Longer-lived refresh tokens (7 days) ✓ Rotate refresh tokens on use ✓ Store refresh tokens in database Storage: ✓ HttpOnly cookies for refresh tokens ✓ Memory for access tokens (not localStorage) ✓ Secure flag in production ✓ SameSite=Strict Validation: ✓ Verify signature, issuer, audience ✓ Check expiration ✓ Validate user still exists ✓ Handle token revocation General: ✓ Use strong secrets (256+ bits) ✓ Different secrets for access/refresh ✓ Implement logout from all devices ✓ Rotate tokens on password change

Conclusion#

JWT authentication requires careful implementation. Use short-lived access tokens with longer-lived refresh tokens. Store refresh tokens in HTTP-only cookies, rotate them on use, and implement proper revocation. Never store JWTs in localStorage—use memory or secure cookies.

Share this article

Help spread the word about Bootspring