Building Secure Authentication with JWT and OAuth 2.0 in Node.js Applications
Introduction
Authentication is the cornerstone of application security, yet many developers struggle with implementing it correctly. In this comprehensive guide, we'll explore how to build secure authentication systems using JSON Web Tokens (JWT) and OAuth 2.0 in Node.js applications, focusing on security best practices that protect against common vulnerabilities.
Understanding JWT vs Session-Based Authentication
Before diving into implementation, it's crucial to understand when to use JWT tokens versus traditional session-based authentication:
- JWT Tokens: Stateless, perfect for distributed systems and APIs
- Sessions: Stateful, better for traditional web applications with server-side rendering
JWT tokens are ideal when you need scalability across multiple servers or when building APIs that serve mobile applications.
Setting Up Secure JWT Authentication
Let's start by creating a secure JWT authentication system. First, install the necessary dependencies:
npm install jsonwebtoken bcryptjs express-rate-limit helmet corsHere's a secure JWT implementation:
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const crypto = require('crypto');
class AuthService {
constructor() {
this.accessTokenSecret = process.env.JWT_ACCESS_SECRET;
this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET;
this.accessTokenExpiry = '15m';
this.refreshTokenExpiry = '7d';
}
async hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async verifyPassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
generateTokenPair(userId, email) {
const payload = { userId, email, type: 'access' };
const refreshPayload = { userId, type: 'refresh', jti: crypto.randomUUID() };
const accessToken = jwt.sign(payload, this.accessTokenSecret, {
expiresIn: this.accessTokenExpiry,
issuer: 'your-app-name',
audience: 'your-app-users'
});
const refreshToken = jwt.sign(refreshPayload, this.refreshTokenSecret, {
expiresIn: this.refreshTokenExpiry,
issuer: 'your-app-name',
audience: 'your-app-users'
});
return { accessToken, refreshToken };
}
verifyAccessToken(token) {
try {
return jwt.verify(token, this.accessTokenSecret, {
issuer: 'your-app-name',
audience: 'your-app-users'
});
} catch (error) {
throw new Error('Invalid or expired access token');
}
}
}Implementing OAuth 2.0 Integration
OAuth 2.0 allows users to authenticate using third-party providers like Google, GitHub, or Facebook. Here's how to implement Google OAuth:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback"
}, async (accessToken, refreshToken, profile, done) => {
try {
// Check if user exists
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// Create new user
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value,
provider: 'google'
});
}
return done(null, user);
} catch (error) {
return done(error, null);
}
}));Security Middleware and Best Practices
Implement comprehensive security middleware to protect your authentication endpoints:
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
// Rate limiting for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts per window
message: 'Too many authentication attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
// JWT Authentication middleware
const authenticateToken = async (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const authService = new AuthService();
const decoded = authService.verifyAccessToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
};
// Apply security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
}));Secure Token Storage and Refresh Strategy
Implement a secure token refresh mechanism:
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
try {
const authService = new AuthService();
const decoded = jwt.verify(refreshToken, authService.refreshTokenSecret);
// Check if refresh token is blacklisted (implement Redis blacklist)
const isBlacklisted = await redis.get(`blacklist:${decoded.jti}`);
if (isBlacklisted) {
return res.status(403).json({ error: 'Token has been revoked' });
}
// Generate new token pair
const user = await User.findById(decoded.userId);
const tokens = authService.generateTokenPair(user.id, user.email);
// Blacklist old refresh token
await redis.setex(`blacklist:${decoded.jti}`, 604800, 'true'); // 7 days
res.json({ success: true, tokens });
} catch (error) {
res.status(403).json({ error: 'Invalid refresh token' });
}
});Common Security Pitfalls to Avoid
- Storing sensitive data in JWT payload: JWT tokens are base64 encoded, not encrypted
- Using weak secrets: Always use cryptographically strong secrets (256+ bits)
- Not implementing token rotation: Refresh tokens should be rotated on each use
- Missing rate limiting: Always implement rate limiting on authentication endpoints
- Improper error handling: Don't leak sensitive information in error messages
Conclusion
Implementing secure authentication requires careful attention to detail and adherence to security best practices. By combining JWT tokens with OAuth 2.0, implementing proper rate limiting, and following secure coding practices, you can build robust authentication systems that protect your users and applications from common security threats.
Remember to regularly update your dependencies, monitor for security vulnerabilities, and consider implementing additional security measures like two-factor authentication for enhanced protection.
Related Posts
Building Secure Authentication with JWT in Node.js: Best Practices for 2024
Learn how to implement bulletproof JWT authentication in Node.js with modern security practices and real-world examples.
Building Secure JWT Authentication with Refresh Tokens in Node.js
Learn to implement bulletproof JWT authentication with refresh token rotation and security best practices for production apps.
Building Bulletproof Authentication with JWT and Refresh Token Strategy
Learn how to implement secure JWT authentication with refresh tokens to protect against common vulnerabilities and attacks.