JSON Web Tokens (JWT) provide a secure and efficient way to implement authentication in web APIs. This guide covers implementing JWT authentication with best practices and security considerations.
Understanding JWT Authentication
JWT enables stateless authentication through signed tokens. Let's implement a secure authentication system.
Basic JWT Implementation
Set up JWT authentication in Express:
const express = require('express')
const jwt = require('jsonwebtoken')
const bcrypt = require('bcrypt')
const app = express()
app.use(express.json())
const JWT_SECRET = process.env.JWT_SECRET
const JWT_EXPIRES_IN = '24h'
async function generateToken(user) {
return jwt.sign(
{
id: user.id,
email: user.email,
role: user.role
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
)
}
app.post('/api/login', async (req, res) => {
const { email, password } = req.body
try {
const user = await User.findOne({ email })
if (!user) {
return res.status(401).json({
error: 'Invalid credentials'
})
}
const isValid = await bcrypt.compare(
password,
user.password
)
if (!isValid) {
return res.status(401).json({
error: 'Invalid credentials'
})
}
const token = await generateToken(user)
res.json({ token })
} catch (error) {
res.status(500).json({
error: 'Authentication failed'
})
}
})
Middleware Implementation
Create authentication middleware:
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1]
if (!token) {
return res.status(401).json({
error: 'Authentication required'
})
}
try {
const decoded = jwt.verify(token, JWT_SECRET)
req.user = decoded
next()
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired'
})
}
res.status(403).json({
error: 'Invalid token'
})
}
}
// Usage
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: 'Protected data',
user: req.user
})
})
Refresh Token Implementation
Implement secure refresh token logic:
const crypto = require('crypto')
class TokenManager {
constructor() {
this.refreshTokens = new Map()
}
async generateRefreshToken(userId) {
const refreshToken = crypto
.randomBytes(40)
.toString('hex')
this.refreshTokens.set(refreshToken, {
userId,
createdAt: new Date()
})
return refreshToken
}
async verifyRefreshToken(token) {
const tokenData = this.refreshTokens.get(token)
if (!tokenData) {
throw new Error('Invalid refresh token')
}
const tokenAge = Date.now() - tokenData.createdAt
if (tokenAge > 7 * 24 * 60 * 60 * 1000) { // 7 days
this.refreshTokens.delete(token)
throw new Error('Refresh token expired')
}
return tokenData.userId
}
invalidateRefreshToken(token) {
this.refreshTokens.delete(token)
}
}
const tokenManager = new TokenManager()
app.post('/api/refresh-token', async (req, res) => {
const { refreshToken } = req.body
try {
const userId = await tokenManager
.verifyRefreshToken(refreshToken)
const user = await User.findById(userId)
const accessToken = await generateToken(user)
const newRefreshToken = await tokenManager
.generateRefreshToken(userId)
tokenManager.invalidateRefreshToken(refreshToken)
res.json({
accessToken,
refreshToken: newRefreshToken
})
} catch (error) {
res.status(401).json({
error: 'Invalid refresh token'
})
}
})
Role-Based Authorization
Implement role-based access control:
const ROLES = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
}
function authorize(roles = []) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: 'Authentication required'
})
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
})
}
next()
}
}
// Usage
app.get(
'/api/admin',
authenticateToken,
authorize([ROLES.ADMIN]),
(req, res) => {
res.json({ message: 'Admin access granted' })
}
)
Security Enhancements
Token Blacklisting
Implement token blacklisting for logout:
class TokenBlacklist {
constructor() {
this.blacklist = new Set()
this.cleanup()
}
add(token, exp) {
this.blacklist.add({
token,
exp: new Date(exp * 1000)
})
}
isBlacklisted(token) {
return Array.from(this.blacklist)
.some(item => item.token === token)
}
cleanup() {
const now = new Date()
this.blacklist = new Set(
Array.from(this.blacklist)
.filter(item => item.exp > now)
)
setTimeout(() => this.cleanup(), 3600000) // 1 hour
}
}
const blacklist = new TokenBlacklist()
app.post('/api/logout', authenticateToken, (req, res) => {
const token = req.headers.authorization.split(' ')[1]
const decoded = jwt.decode(token)
blacklist.add(token, decoded.exp)
res.json({ message: 'Logged out successfully' })
})
Rate Limiting
Implement rate limiting for authentication endpoints:
const rateLimit = require('express-rate-limit')
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: {
error: 'Too many login attempts. Please try again later.'
}
})
app.post('/api/login', authLimiter, async (req, res) => {
// Login logic
})
Error Handling
Implement comprehensive error handling:
class AuthError extends Error {
constructor(message, status = 401) {
super(message)
this.name = 'AuthError'
this.status = status
}
}
function handleAuthError(error, req, res, next) {
if (error instanceof AuthError) {
return res.status(error.status).json({
error: error.message
})
}
next(error)
}
app.use(handleAuthError)
Testing Authentication
Implement authentication tests:
const request = require('supertest')
const app = require('./app')
describe('Authentication', () => {
let token
beforeEach(async () => {
const response = await request(app)
.post('/api/login')
.send({
email: 'test@example.com',
password: 'password123'
})
token = response.body.token
})
test('protected route requires token', async () => {
const response = await request(app)
.get('/api/protected')
expect(response.status).toBe(401)
})
test('protected route accepts valid token', async () => {
const response = await request(app)
.get('/api/protected')
.set('Authorization', `Bearer ${token}`)
expect(response.status).toBe(200)
})
})
Best Practices
- Token Security
- Use strong secrets
- Implement token expiration
- Secure token storage
- Implement refresh tokens
- Implementation
- Use HTTPS
- Implement rate limiting
- Handle errors properly
- Validate inputs
- Testing
- Test authentication flows
- Verify token validation
- Test error cases
- Check security measures
Conclusion
JWT authentication provides a secure way to implement API authentication:
- Stateless authentication
- Role-based access control
- Refresh token support
- Security features
Key takeaways:
- Implement proper token management
- Use security best practices
- Handle errors appropriately
- Test thoroughly
- Monitor and maintain security
By following these patterns and best practices, you can implement secure and maintainable JWT authentication in your APIs.