Building Secure Authentication with JWT and OAuth 2.0: A Complete Implementation Guide
Introduction
Authentication is the backbone of modern web applications, yet it's one of the most commonly misconfigured security components. As developers, we often rush to implement basic login functionality without considering the security implications. Today, I'll walk you through building a robust authentication system using JWT (JSON Web Tokens) and OAuth 2.0, focusing on security best practices that protect both your users and your application.
Understanding JWT Security Fundamentals
JWTs are stateless tokens that carry user information and claims. However, their convenience comes with security responsibilities that many developers overlook.
JWT Structure and Security Considerations
A JWT consists of three parts: header, payload, and signature. The key security principle is that while the payload is readable, the signature ensures integrity.
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
// Generate a secure secret key
const JWT_SECRET = crypto.randomBytes(64).toString('hex');
// Create JWT with proper expiration
function generateTokens(userId, userRole) {
const payload = {
userId: userId,
role: userRole,
iat: Math.floor(Date.now() / 1000),
iss: 'your-app-name'
};
const accessToken = jwt.sign(payload, JWT_SECRET, {
expiresIn: '15m',
algorithm: 'HS256'
});
const refreshToken = jwt.sign(
{ userId: userId, tokenType: 'refresh' },
JWT_SECRET,
{ expiresIn: '7d', algorithm: 'HS256' }
);
return { accessToken, refreshToken };
}Implementing Secure Token Storage
Never store JWTs in localStorage. Instead, use secure HTTP-only cookies for web applications:
// Express.js middleware for secure cookie handling
function setAuthCookies(res, accessToken, refreshToken) {
// Access token in HTTP-only cookie
res.cookie('accessToken', accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 15 * 60 * 1000 // 15 minutes
});
// Refresh token in separate HTTP-only cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/auth/refresh' // Restrict to refresh endpoint
});
}Implementing OAuth 2.0 Authorization
OAuth 2.0 provides a secure way to handle third-party authentication. Here's how to implement it properly with Google OAuth:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Configure Google OAuth strategy
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback',
scope: ['profile', 'email']
}, async (accessToken, refreshToken, profile, done) => {
try {
// Check if user exists
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// Create new user with OAuth data
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value,
emailVerified: true // OAuth providers verify emails
});
}
return done(null, user);
} catch (error) {
return done(error, null);
}
}));Building a Secure Authentication Middleware
Create middleware that validates tokens and handles common security scenarios:
function authenticateToken(req, res, next) {
const token = req.cookies.accessToken;
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED'
});
}
return res.status(403).json({ error: 'Invalid token' });
}
// Add rate limiting per user
const userId = decoded.userId;
if (isRateLimited(userId)) {
return res.status(429).json({ error: 'Too many requests' });
}
req.user = decoded;
next();
});
}
// Token refresh endpoint
app.post('/auth/refresh', (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' });
}
jwt.verify(refreshToken, JWT_SECRET, async (err, decoded) => {
if (err) {
return res.status(403).json({ error: 'Invalid refresh token' });
}
// Verify token hasn't been revoked
const isRevoked = await checkTokenRevocation(decoded.userId, refreshToken);
if (isRevoked) {
return res.status(403).json({ error: 'Token revoked' });
}
const newTokens = generateTokens(decoded.userId, decoded.role);
setAuthCookies(res, newTokens.accessToken, newTokens.refreshToken);
res.json({ message: 'Tokens refreshed successfully' });
});
});Essential Security Measures
Input Validation and Sanitization
Always validate and sanitize user inputs to prevent injection attacks:
const validator = require('validator');
const rateLimit = require('express-rate-limit');
// 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',
standardHeaders: true,
legacyHeaders: false
});
// Login endpoint with validation
app.post('/auth/login', authLimiter, async (req, res) => {
const { email, password } = req.body;
// Validate input
if (!validator.isEmail(email)) {
return res.status(400).json({ error: 'Invalid email format' });
}
if (!password || password.length < 8) {
return res.status(400).json({ error: 'Password must be at least 8 characters' });
}
try {
const user = await User.findOne({ email: validator.normalizeEmail(email) });
if (!user || !(await bcrypt.compare(password, user.hashedPassword))) {
// Use same response time to prevent user enumeration
await new Promise(resolve => setTimeout(resolve, 100));
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = generateTokens(user.id, user.role);
setAuthCookies(res, tokens.accessToken, tokens.refreshToken);
res.json({ message: 'Login successful', user: { id: user.id, email: user.email } });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Frontend Security Implementation
On the client side, handle authentication state securely:
// React hook for secure authentication
import { useState, useEffect, createContext, useContext } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuthStatus();
}, []);
const checkAuthStatus = async () => {
try {
const response = await fetch('/api/auth/me', {
credentials: 'include' // Include cookies
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
}
} catch (error) {
console.error('Auth check failed:', error);
} finally {
setLoading(false);
}
};
const logout = async () => {
try {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include'
});
setUser(null);
} catch (error) {
console.error('Logout failed:', error);
}
};
return (
{children}
);
}Conclusion
Implementing secure authentication requires attention to multiple layers: proper JWT handling, secure token storage, robust OAuth integration, input validation, and rate limiting. Remember that security is not a one-time implementation but an ongoing process. Regularly audit your authentication system, keep dependencies updated, and stay informed about emerging security threats. The extra effort invested in proper authentication security will protect your users and your application from costly breaches.
Related Posts
Building Secure REST APIs: A Complete Authentication and Authorization Guide
Master API security with JWT tokens, role-based access control, and essential security practices for production applications.
Building Secure User Authentication with JWT and Refresh Tokens in Node.js
Learn to implement bulletproof JWT authentication with refresh token rotation and security best practices in Node.js applications.
Building a Secure JWT Authentication System with Express.js and bcrypt
Learn to implement bulletproof JWT authentication in Node.js with proper password hashing, token management, and security best practices.