Authentication and Authorization: JWT, OAuth 2.0, and Session Management
Master authentication and authorization with JWT, OAuth 2.0, and session management. Learn security best practices, implementation patterns, and when to use each approach.
Table of Contents
- Introduction
- Understanding Authentication vs Authorization
- Session-Based Authentication
- Token-Based Authentication with JWT
- OAuth 2.0 and OpenID Connect
- Comparing Authentication Methods
- Security Best Practices
- Implementation Examples
- Common Pitfalls and Anti-Patterns
- Conclusion
Introduction
Authentication and authorization are the foundation of secure web applications. Every application that handles user data needs to verify who users are (authentication) and control what they can access (authorization). Yet, implementing these systems correctly is one of the most challenging aspects of backend development, with security implications that can make or break your application.
The web development ecosystem offers multiple approaches to authentication: traditional session-based authentication using cookies, modern token-based authentication with JSON Web Tokens (JWT), and delegated authentication through OAuth 2.0. Each approach has its strengths, trade-offs, and ideal use cases. Understanding when and how to use each method is crucial for building secure, scalable applications.
This comprehensive guide will teach you everything you need to know about authentication and authorization. You’ll learn how session-based authentication works, understand JWT tokens and their security implications, master OAuth 2.0 flows, and discover best practices for implementing each approach securely. By the end, you’ll be able to choose the right authentication strategy for your application and implement it correctly.
Understanding Authentication vs Authorization
Before diving into specific authentication methods, it’s essential to understand the fundamental difference between authentication and authorization, as these terms are often confused.
Authentication: Who Are You?
Authentication is the process of verifying a user’s identity. It answers the question: “Who are you?” Authentication typically involves:
- Credentials: Username/password, email/password, or biometric data
- Verification: Checking credentials against stored data
- Session Creation: Establishing a trusted session after successful verification
// Authentication example: Verifying user credentialsasync function authenticateUser(email, password) { // 1. Find user by email const user = await db.users.findOne({ email });
if (!user) { throw new Error("Invalid credentials"); }
// 2. Verify password const isValid = await bcrypt.compare(password, user.hashedPassword);
if (!isValid) { throw new Error("Invalid credentials"); }
// 3. Authentication successful - user identity verified return user;}Authorization: What Can You Do?
Authorization is the process of determining what resources and actions a user can access. It answers the question: “What are you allowed to do?” Authorization typically involves:
- Roles: Admin, User, Moderator, etc.
- Permissions: Specific actions like “read”, “write”, “delete”
- Resource Access: Which resources a user can access
// Authorization example: Checking if user can access a resourcefunction authorizeUser(user, resource, action) { // Check user role if (user.role === "admin") { return true; // Admins can do anything }
// Check specific permissions if (user.permissions.includes(`${resource}:${action}`)) { return true; }
// Check resource ownership if (resource.ownerId === user.id) { return true; }
return false; // Access denied}The Relationship Between Authentication and Authorization
Authentication must happen before authorization. You can’t determine what a user can do until you know who they are:
// Complete flow: Authentication → Authorizationasync function handleRequest(req, res) { try { // Step 1: Authenticate - verify who the user is const user = await authenticateUser(req.body.email, req.body.password);
// Step 2: Authorize - check what they can do if (!authorizeUser(user, "posts", "create")) { return res.status(403).json({ error: "Insufficient permissions", }); }
// Step 3: Perform authorized action const post = await createPost(req.body.content, user.id); res.json(post); } catch (error) { res.status(401).json({ error: "Authentication failed" }); }}Session-Based Authentication
Session-based authentication is the traditional approach where the server maintains session state and uses cookies to identify authenticated users. This method has been the standard for web applications for decades.
How Session-Based Authentication Works
Session-based authentication follows this flow:
- User Login: User submits credentials
- Server Verification: Server verifies credentials
- Session Creation: Server creates a session and stores it
- Cookie Set: Server sends session ID in a cookie
- Subsequent Requests: Browser automatically sends cookie
- Session Validation: Server validates session on each request
// Express.js session-based authentication exampleconst express = require("express");const session = require("express-session");const bcrypt = require("bcrypt");
const app = express();
// Configure session middlewareapp.use( session({ secret: "your-secret-key-change-in-production", resave: false, saveUninitialized: false, cookie: { secure: true, // HTTPS only httpOnly: true, // Prevent XSS attacks maxAge: 24 * 60 * 60 * 1000, // 24 hours }, }),);
// Login endpointapp.post("/login", async (req, res) => { const { email, password } = req.body;
// Find user const user = await db.users.findOne({ email }); if (!user) { return res.status(401).json({ error: "Invalid credentials" }); }
// Verify password const isValid = await bcrypt.compare(password, user.hashedPassword); if (!isValid) { return res.status(401).json({ error: "Invalid credentials" }); }
// Create session req.session.userId = user.id; req.session.email = user.email; req.session.role = user.role;
res.json({ message: "Login successful", user: { id: user.id, email: user.email }, });});
// Protected route middlewarefunction requireAuth(req, res, next) { if (!req.session.userId) { return res.status(401).json({ error: "Authentication required" }); } next();}
// Protected routeapp.get("/profile", requireAuth, async (req, res) => { const user = await db.users.findById(req.session.userId); res.json(user);});
// Logout endpointapp.post("/logout", (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ error: "Logout failed" }); } res.clearCookie("connect.sid"); res.json({ message: "Logout successful" }); });});Session Storage Options
Sessions can be stored in different ways, each with trade-offs:
1. In-Memory Storage (Default)
// ❌ Not suitable for production - lost on server restartapp.use( session({ secret: "secret-key", }),);2. Database Storage
// ✅ Persistent across server restartsconst MySQLStore = require("express-mysql-session")(session);
app.use( session({ secret: "secret-key", store: new MySQLStore({ host: "localhost", port: 3306, user: "root", password: "password", database: "sessions", }), }),);3. Redis Storage (Recommended for Production)
// ✅ Fast, scalable, supports clusteringconst RedisStore = require("connect-redis")(session);const redis = require("redis");
const redisClient = redis.createClient({ host: "localhost", port: 6379,});
app.use( session({ secret: "secret-key", store: new RedisStore({ client: redisClient }), resave: false, saveUninitialized: false, }),);Advantages of Session-Based Authentication
✅ Server Control: Server can invalidate sessions immediately ✅ Security: Sensitive data never leaves the server ✅ Simple: Easy to understand and implement ✅ Stateful: Server knows about active sessions ✅ CSRF Protection: Can use CSRF tokens effectively
Disadvantages of Session-Based Authentication
❌ Scalability: Requires shared session storage in distributed systems ❌ Cookie Limitations: CORS issues with cross-domain requests ❌ Mobile Apps: Cookies don’t work well with native mobile apps ❌ Stateless APIs: Doesn’t work well for stateless REST APIs
Token-Based Authentication with JWT
JSON Web Tokens (JWT) provide a stateless authentication mechanism where the token itself contains all necessary information. This makes JWT ideal for distributed systems and stateless APIs.
Understanding JWT Structure
A JWT consists of three parts separated by dots:
header.payload.signature1. Header: Contains token type and signing algorithm
{ "alg": "HS256", "typ": "JWT"}2. Payload: Contains claims (user data)
{ "sub": "1234567890", "email": "user@example.com", "role": "user", "iat": 1516239022, "exp": 1516242622}3. Signature: Used to verify token integrity
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)Implementing JWT Authentication
const jwt = require("jsonwebtoken");const bcrypt = require("bcrypt");
const SECRET_KEY = process.env.JWT_SECRET || "change-this-secret";
// Login endpoint - issue JWTapp.post("/login", async (req, res) => { const { email, password } = req.body;
// Find and verify user const user = await db.users.findOne({ email }); if (!user) { return res.status(401).json({ error: "Invalid credentials" }); }
const isValid = await bcrypt.compare(password, user.hashedPassword); if (!isValid) { return res.status(401).json({ error: "Invalid credentials" }); }
// Create JWT payload const payload = { userId: user.id, email: user.email, role: user.role, };
// Sign token with expiration const token = jwt.sign(payload, SECRET_KEY, { expiresIn: "24h", // Token expires in 24 hours issuer: "your-app-name", audience: "your-app-users", });
res.json({ token, user: { id: user.id, email: user.email, role: user.role, }, });});
// Middleware to verify JWTfunction authenticateToken(req, res, next) { // Get token from Authorization header const authHeader = req.headers["authorization"]; const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
if (!token) { return res.status(401).json({ error: "Access token required" }); }
// Verify token jwt.verify(token, SECRET_KEY, (err, decoded) => { if (err) { return res.status(403).json({ error: "Invalid or expired token" }); }
// Attach user info to request req.user = decoded; next(); });}
// Protected routeapp.get("/profile", authenticateToken, async (req, res) => { const user = await db.users.findById(req.user.userId); res.json(user);});JWT Refresh Tokens
For better security, implement refresh tokens alongside access tokens:
// Login with refresh tokenapp.post("/login", async (req, res) => { const { email, password } = req.body;
// ... verify credentials ...
// Short-lived access token (15 minutes) const accessToken = jwt.sign( { userId: user.id, email: user.email }, SECRET_KEY, { expiresIn: "15m" }, );
// Long-lived refresh token (7 days) const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: "7d", });
// Store refresh token in database await db.refreshTokens.create({ userId: user.id, token: refreshToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), });
res.json({ accessToken, refreshToken, expiresIn: 900, // 15 minutes in seconds });});
// Refresh access token endpointapp.post("/refresh", async (req, res) => { const { refreshToken } = req.body;
if (!refreshToken) { return res.status(401).json({ error: "Refresh token required" }); }
// Verify refresh token let decoded; try { decoded = jwt.verify(refreshToken, REFRESH_SECRET); } catch (err) { return res.status(403).json({ error: "Invalid refresh token" }); }
// Check if refresh token exists in database const storedToken = await db.refreshTokens.findOne({ userId: decoded.userId, token: refreshToken, });
if (!storedToken || storedToken.expiresAt < new Date()) { return res.status(403).json({ error: "Refresh token expired" }); }
// Get user data const user = await db.users.findById(decoded.userId);
// Issue new access token const accessToken = jwt.sign( { userId: user.id, email: user.email }, SECRET_KEY, { expiresIn: "15m" }, );
res.json({ accessToken, expiresIn: 900, });});Advantages of JWT
✅ Stateless: No server-side session storage needed ✅ Scalable: Works across multiple servers without shared storage ✅ Cross-Domain: Works with CORS and mobile apps ✅ Self-Contained: Token contains user information ✅ Flexible: Can include custom claims
Disadvantages of JWT
❌ Token Size: Larger than session IDs (can impact performance) ❌ Revocation: Hard to invalidate tokens before expiration ❌ Security: If secret is compromised, all tokens are compromised ❌ No Server Control: Can’t easily revoke or update tokens ❌ XSS Vulnerability: Stored in localStorage, vulnerable to XSS
OAuth 2.0 and OpenID Connect
OAuth 2.0 is an authorization framework that allows third-party applications to obtain limited access to user accounts. OpenID Connect (OIDC) extends OAuth 2.0 to provide authentication capabilities.
Understanding OAuth 2.0 Flows
OAuth 2.0 defines several authorization flows for different use cases:
1. Authorization Code Flow (Most Common) Best for web applications with a backend server.
2. Implicit Flow (Deprecated) Previously used for single-page applications, now deprecated.
3. Client Credentials Flow For server-to-server communication.
4. Resource Owner Password Credentials Flow For trusted applications only.
5. Device Flow For devices with limited input capabilities.
Authorization Code Flow Implementation
// OAuth 2.0 Authorization Server (Provider)const express = require("express");const crypto = require("crypto");
const app = express();
// Store authorization codes temporarilyconst authCodes = new Map();
// Step 1: Authorization endpointapp.get("/oauth/authorize", (req, res) => { const { client_id, redirect_uri, response_type, scope, state } = req.query;
// Validate client if (!validateClient(client_id, redirect_uri)) { return res.status(400).json({ error: "invalid_client" }); }
// Check if user is authenticated (simplified) if (!req.session.userId) { // Redirect to login page return res.redirect(`/login?redirect=${encodeURIComponent(req.url)}`); }
// Generate authorization code const authCode = crypto.randomBytes(32).toString("hex"); authCodes.set(authCode, { userId: req.session.userId, clientId: client_id, redirectUri: redirect_uri, scope: scope, expiresAt: Date.now() + 600000, // 10 minutes });
// Redirect back to client with authorization code res.redirect(`${redirect_uri}?code=${authCode}&state=${state}`);});
// Step 2: Token endpointapp.post("/oauth/token", async (req, res) => { const { grant_type, code, redirect_uri, client_id, client_secret } = req.body;
if (grant_type !== "authorization_code") { return res.status(400).json({ error: "unsupported_grant_type" }); }
// Validate client credentials if (!validateClientCredentials(client_id, client_secret)) { return res.status(401).json({ error: "invalid_client" }); }
// Validate authorization code const authCodeData = authCodes.get(code); if (!authCodeData || authCodeData.expiresAt < Date.now()) { return res.status(400).json({ error: "invalid_grant" }); }
// Validate redirect URI matches if (authCodeData.redirectUri !== redirect_uri) { return res.status(400).json({ error: "invalid_grant" }); }
// Delete used authorization code authCodes.delete(code);
// Generate access token const accessToken = jwt.sign( { userId: authCodeData.userId, clientId: client_id, scope: authCodeData.scope, }, SECRET_KEY, { expiresIn: "1h" }, );
// Generate refresh token const refreshToken = jwt.sign( { userId: authCodeData.userId, clientId: client_id }, REFRESH_SECRET, { expiresIn: "30d" }, );
res.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600, refresh_token: refreshToken, scope: authCodeData.scope, });});OAuth 2.0 Client Implementation
// OAuth 2.0 Client Applicationclass OAuth2Client { constructor(config) { this.clientId = config.clientId; this.clientSecret = config.clientSecret; this.redirectUri = config.redirectUri; this.authorizationUrl = config.authorizationUrl; this.tokenUrl = config.tokenUrl; }
// Step 1: Redirect user to authorization server getAuthorizationUrl(state) { const params = new URLSearchParams({ response_type: "code", client_id: this.clientId, redirect_uri: this.redirectUri, scope: "read write", state: state, });
return `${this.authorizationUrl}?${params.toString()}`; }
// Step 2: Exchange authorization code for tokens async exchangeCode(code) { const response = await fetch(this.tokenUrl, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", Authorization: `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`, }, body: new URLSearchParams({ grant_type: "authorization_code", code: code, redirect_uri: this.redirectUri, }), });
return await response.json(); }
// Use access token to make API requests async makeAuthenticatedRequest(url, accessToken) { const response = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}`, }, });
return await response.json(); }}
// Usage in Express appapp.get("/oauth/callback", async (req, res) => { const { code, state } = req.query;
// Verify state to prevent CSRF if (state !== req.session.oauthState) { return res.status(400).json({ error: "Invalid state" }); }
const oauthClient = new OAuth2Client({ clientId: process.env.OAUTH_CLIENT_ID, clientSecret: process.env.OAUTH_CLIENT_SECRET, redirectUri: "http://localhost:3000/oauth/callback", authorizationUrl: "https://oauth-provider.com/oauth/authorize", tokenUrl: "https://oauth-provider.com/oauth/token", });
try { const tokens = await oauthClient.exchangeCode(code);
// Store tokens securely (in database, encrypted) await db.userTokens.create({ userId: req.session.userId, accessToken: encrypt(tokens.access_token), refreshToken: encrypt(tokens.refresh_token), expiresAt: new Date(Date.now() + tokens.expires_in * 1000), });
res.redirect("/dashboard"); } catch (error) { res.status(500).json({ error: "OAuth flow failed" }); }});OpenID Connect (OIDC)
OpenID Connect extends OAuth 2.0 to provide authentication. It adds an ID token (JWT) that contains user identity information:
// OIDC adds ID token to OAuth 2.0 responseapp.post("/oauth/token", async (req, res) => { // ... OAuth 2.0 token generation ...
// Generate ID token (OpenID Connect) const idToken = jwt.sign( { iss: "https://your-provider.com", // Issuer sub: authCodeData.userId, // Subject (user ID) aud: client_id, // Audience exp: Math.floor(Date.now() / 1000) + 3600, // Expiration iat: Math.floor(Date.now() / 1000), // Issued at email: user.email, name: user.name, }, SECRET_KEY, { algorithm: "HS256" }, );
res.json({ access_token: accessToken, token_type: "Bearer", expires_in: 3600, id_token: idToken, // OpenID Connect addition refresh_token: refreshToken, });});Advantages of OAuth 2.0
✅ Delegated Authorization: Users can grant limited access without sharing passwords ✅ Standardized: Widely adopted standard ✅ Flexible: Multiple flows for different use cases ✅ Third-Party Integration: Perfect for “Sign in with Google/Facebook” ✅ Revocable: Users can revoke access at any time
Disadvantages of OAuth 2.0
❌ Complexity: More complex than simple authentication ❌ Implementation Risk: Easy to implement incorrectly ❌ Token Management: Requires secure token storage ❌ User Experience: Multiple redirects can confuse users
Comparing Authentication Methods
Each authentication method has its ideal use case. Here’s a comparison to help you choose:
| Feature | Sessions | JWT | OAuth 2.0 |
|---|---|---|---|
| Stateless | ❌ No | ✅ Yes | ✅ Yes |
| Scalability | ⚠️ Requires shared storage | ✅ Excellent | ✅ Excellent |
| Mobile Apps | ❌ Poor | ✅ Good | ✅ Excellent |
| Cross-Domain | ⚠️ Complex | ✅ Good | ✅ Excellent |
| Revocation | ✅ Immediate | ❌ Difficult | ✅ Possible |
| Security | ✅ High | ⚠️ Medium | ✅ High |
| Complexity | ✅ Low | ✅ Medium | ❌ High |
| Use Case | Traditional web apps | APIs, SPAs | Third-party integration |
When to Use Session-Based Authentication
✅ Traditional Web Applications: Server-rendered apps with cookies ✅ Single Domain: All requests from same domain ✅ Server Control: Need immediate session revocation ✅ CSRF Protection: Can use CSRF tokens effectively
// Example: E-commerce website// Sessions work perfectly for shopping carts, user preferencesapp.use( session({ secret: process.env.SESSION_SECRET, store: new RedisStore({ client: redisClient }), }),);When to Use JWT
✅ REST APIs: Stateless API endpoints ✅ Microservices: Distributed systems without shared storage ✅ Mobile Apps: Native mobile applications ✅ SPAs: Single-page applications with separate API
// Example: Mobile app backend API// JWT tokens work great for mobile appsapp.post("/api/login", async (req, res) => { // ... verify credentials ... const token = jwt.sign({ userId: user.id }, SECRET_KEY); res.json({ token });});When to Use OAuth 2.0
✅ Third-Party Integration: “Sign in with Google/Facebook” ✅ API Access: Allowing third parties to access your API ✅ Delegated Authorization: Users granting access to other apps ✅ Enterprise SSO: Single sign-on for organizations
// Example: Allowing users to connect their Google accountapp.get("/auth/google", (req, res) => { const authUrl = oauthClient.getAuthorizationUrl(); res.redirect(authUrl);});Security Best Practices
Regardless of which authentication method you choose, following security best practices is crucial.
Password Security
✅ Hash Passwords: Never store plain text passwords
const bcrypt = require("bcrypt");const saltRounds = 12;
// Hash password before storingconst hashedPassword = await bcrypt.hash(password, saltRounds);
// Verify passwordconst isValid = await bcrypt.compare(password, hashedPassword);✅ Use Strong Passwords: Enforce password policies
function validatePassword(password) { const minLength = 8; const hasUpperCase = /[A-Z]/.test(password); const hasLowerCase = /[a-z]/.test(password); const hasNumbers = /\d/.test(password); const hasSpecialChar = /[!@#$%^&*]/.test(password);
return ( password.length >= minLength && hasUpperCase && hasLowerCase && hasNumbers && hasSpecialChar );}✅ Rate Limiting: Prevent brute force attacks
const rateLimit = require("express-rate-limit");
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts per window message: "Too many login attempts, please try again later",});
app.post("/login", loginLimiter, async (req, res) => { // ... login logic ...});Token Security
✅ Use HTTPS: Always use HTTPS in production
// Enforce HTTPSif (process.env.NODE_ENV === "production") { app.use((req, res, next) => { if (req.header("x-forwarded-proto") !== "https") { res.redirect(`https://${req.header("host")}${req.url}`); } else { next(); } });}✅ Short Token Expiration: Use short-lived access tokens
// Access token: 15 minutesconst accessToken = jwt.sign(payload, SECRET_KEY, { expiresIn: "15m",});
// Refresh token: 7 daysconst refreshToken = jwt.sign(payload, REFRESH_SECRET, { expiresIn: "7d",});✅ Secure Token Storage: Store tokens securely
// ❌ Don't store in localStorage (vulnerable to XSS)localStorage.setItem("token", token);
// ✅ Store in httpOnly cookie (more secure)res.cookie("token", token, { httpOnly: true, secure: true, sameSite: "strict",});✅ Token Rotation: Rotate refresh tokens
app.post("/refresh", async (req, res) => { const { refreshToken } = req.body;
// Verify old refresh token const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
// Delete old refresh token await db.refreshTokens.delete({ token: refreshToken });
// Issue new access token and refresh token const newAccessToken = jwt.sign(payload, SECRET_KEY, { expiresIn: "15m" }); const newRefreshToken = jwt.sign(payload, REFRESH_SECRET, { expiresIn: "7d", });
// Store new refresh token await db.refreshTokens.create({ token: newRefreshToken, userId: decoded.userId, });
res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken, });});Session Security
✅ Secure Cookies: Configure cookies securely
app.use( session({ secret: process.env.SESSION_SECRET, cookie: { secure: true, // HTTPS only httpOnly: true, // Prevent XSS sameSite: "strict", // CSRF protection maxAge: 24 * 60 * 60 * 1000, // 24 hours }, }),);✅ Session Regeneration: Regenerate session ID after login
app.post("/login", async (req, res) => { // ... verify credentials ...
// Regenerate session to prevent session fixation req.session.regenerate((err) => { if (err) throw err;
req.session.userId = user.id; res.json({ message: "Login successful" }); });});Additional Security Measures
✅ CSRF Protection: Protect against CSRF attacks
const csrf = require("csurf");const csrfProtection = csrf({ cookie: true });
app.use(csrfProtection);
// Include CSRF token in formsapp.get("/form", (req, res) => { res.render("form", { csrfToken: req.csrfToken() });});✅ CORS Configuration: Configure CORS properly
const cors = require("cors");
app.use( cors({ origin: process.env.ALLOWED_ORIGINS.split(","), credentials: true, methods: ["GET", "POST", "PUT", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], }),);✅ Security Headers: Add security headers
const helmet = require("helmet");
app.use( helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], }, }, }),);Implementation Examples
Let’s look at complete implementation examples for each authentication method.
Complete Session-Based Authentication
const express = require("express");const session = require("express-session");const RedisStore = require("connect-redis")(session);const redis = require("redis");const bcrypt = require("bcrypt");const rateLimit = require("express-rate-limit");
const app = express();const redisClient = redis.createClient();
app.use(express.json());app.use(express.urlencoded({ extended: true }));
// Session configurationapp.use( session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, name: "sessionId", // Don't use default 'connect.sid' cookie: { secure: process.env.NODE_ENV === "production", httpOnly: true, sameSite: "strict", maxAge: 24 * 60 * 60 * 1000, // 24 hours }, }),);
// Rate limitingconst loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, message: "Too many login attempts",});
// Registrationapp.post("/register", async (req, res) => { const { email, password, name } = req.body;
// Validate input if (!email || !password || !name) { return res.status(400).json({ error: "All fields required" }); }
// Check if user exists const existingUser = await db.users.findOne({ email }); if (existingUser) { return res.status(409).json({ error: "User already exists" }); }
// Hash password const hashedPassword = await bcrypt.hash(password, 12);
// Create user const user = await db.users.create({ email, password: hashedPassword, name, });
res.status(201).json({ message: "User created", userId: user.id });});
// Loginapp.post("/login", loginLimiter, async (req, res) => { const { email, password } = req.body;
// Find user const user = await db.users.findOne({ email }); if (!user) { return res.status(401).json({ error: "Invalid credentials" }); }
// Verify password const isValid = await bcrypt.compare(password, user.password); if (!isValid) { return res.status(401).json({ error: "Invalid credentials" }); }
// Regenerate session req.session.regenerate((err) => { if (err) { return res.status(500).json({ error: "Login failed" }); }
req.session.userId = user.id; req.session.email = user.email; req.session.role = user.role;
res.json({ message: "Login successful", user: { id: user.id, email: user.email, name: user.name, }, }); });});
// Authentication middlewarefunction requireAuth(req, res, next) { if (!req.session.userId) { return res.status(401).json({ error: "Authentication required" }); } next();}
// Protected routeapp.get("/profile", requireAuth, async (req, res) => { const user = await db.users.findById(req.session.userId); res.json({ id: user.id, email: user.email, name: user.name, role: user.role, });});
// Logoutapp.post("/logout", (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ error: "Logout failed" }); } res.clearCookie("sessionId"); res.json({ message: "Logout successful" }); });});
app.listen(3000, () => { console.log("Server running on port 3000");});Complete JWT Authentication
const express = require("express");const jwt = require("jsonwebtoken");const bcrypt = require("bcrypt");const rateLimit = require("express-rate-limit");
const app = express();const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
app.use(express.json());
// Rate limitingconst loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5,});
// Registerapp.post("/register", async (req, res) => { const { email, password, name } = req.body;
if (!email || !password || !name) { return res.status(400).json({ error: "All fields required" }); }
const existingUser = await db.users.findOne({ email }); if (existingUser) { return res.status(409).json({ error: "User already exists" }); }
const hashedPassword = await bcrypt.hash(password, 12); const user = await db.users.create({ email, password: hashedPassword, name, });
res.status(201).json({ message: "User created", userId: user.id });});
// Loginapp.post("/login", loginLimiter, async (req, res) => { const { email, password } = req.body;
const user = await db.users.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" }); }
// Generate tokens const accessToken = jwt.sign( { userId: user.id, email: user.email, role: user.role }, ACCESS_SECRET, { expiresIn: "15m" }, );
const refreshToken = jwt.sign({ userId: user.id }, REFRESH_SECRET, { expiresIn: "7d", });
// Store refresh token await db.refreshTokens.create({ userId: user.id, token: refreshToken, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), });
res.json({ accessToken, refreshToken, expiresIn: 900, user: { id: user.id, email: user.email, name: user.name, }, });});
// Refresh tokenapp.post("/refresh", async (req, res) => { const { refreshToken } = req.body;
if (!refreshToken) { return res.status(401).json({ error: "Refresh token required" }); }
let decoded; try { decoded = jwt.verify(refreshToken, REFRESH_SECRET); } catch (err) { return res.status(403).json({ error: "Invalid refresh token" }); }
const storedToken = await db.refreshTokens.findOne({ userId: decoded.userId, token: refreshToken, });
if (!storedToken || storedToken.expiresAt < new Date()) { return res.status(403).json({ error: "Refresh token expired" }); }
const user = await db.users.findById(decoded.userId);
// Issue new access token const accessToken = jwt.sign( { userId: user.id, email: user.email, role: user.role }, ACCESS_SECRET, { expiresIn: "15m" }, );
res.json({ accessToken, expiresIn: 900, });});
// Authentication middlewarefunction authenticateToken(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" }); }
jwt.verify(token, ACCESS_SECRET, (err, decoded) => { if (err) { return res.status(403).json({ error: "Invalid or expired token" }); } req.user = decoded; next(); });}
// Protected routeapp.get("/profile", authenticateToken, async (req, res) => { const user = await db.users.findById(req.user.userId); res.json({ id: user.id, email: user.email, name: user.name, role: user.role, });});
// Logout (revoke refresh token)app.post("/logout", authenticateToken, async (req, res) => { const { refreshToken } = req.body;
if (refreshToken) { await db.refreshTokens.delete({ token: refreshToken }); }
res.json({ message: "Logout successful" });});
app.listen(3000, () => { console.log("Server running on port 3000");});Common Pitfalls and Anti-Patterns
Avoid these common mistakes when implementing authentication:
❌ Storing Passwords in Plain Text
// ❌ NEVER do thisawait db.users.create({ email: email, password: password, // Plain text password!});
// ✅ Always hash passwordsconst hashedPassword = await bcrypt.hash(password, 12);await db.users.create({ email: email, password: hashedPassword,});❌ Weak Session Secrets
// ❌ Weak secretapp.use( session({ secret: "secret123", // Too weak! }),);
// ✅ Strong, random secret from environmentapp.use( session({ secret: process.env.SESSION_SECRET, // Long, random string }),);❌ JWT Secret in Code
// ❌ Secret in codeconst token = jwt.sign(payload, "my-secret-key");
// ✅ Secret from environmentconst token = jwt.sign(payload, process.env.JWT_SECRET);❌ Storing JWT in localStorage
// ❌ Vulnerable to XSSlocalStorage.setItem("token", token);
// ✅ Use httpOnly cookies or secure storageres.cookie("token", token, { httpOnly: true, secure: true, sameSite: "strict",});❌ Not Validating Tokens
// ❌ Trusting token without validationconst decoded = jwt.decode(token); // Doesn't verify signature!
// ✅ Always verify tokensconst decoded = jwt.verify(token, SECRET_KEY);❌ Long Token Expiration
// ❌ Tokens that never expireconst token = jwt.sign(payload, SECRET_KEY); // No expiration!
// ✅ Short-lived access tokensconst token = jwt.sign(payload, SECRET_KEY, { expiresIn: "15m", // 15 minutes});❌ Not Handling Token Expiration
// ❌ No error handlingapp.get("/profile", (req, res) => { const decoded = jwt.verify(req.headers.authorization, SECRET_KEY); // Will crash if token is expired});
// ✅ Proper error handlingapp.get("/profile", (req, res) => { try { const token = req.headers.authorization.split(" ")[1]; const decoded = jwt.verify(token, SECRET_KEY); // Use decoded data } catch (err) { if (err.name === "TokenExpiredError") { return res.status(401).json({ error: "Token expired" }); } return res.status(403).json({ error: "Invalid token" }); }});❌ Not Implementing Rate Limiting
// ❌ No rate limiting - vulnerable to brute forceapp.post("/login", async (req, res) => { // Login logic});
// ✅ Rate limiting to prevent brute forceconst loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5,});
app.post("/login", loginLimiter, async (req, res) => { // Login logic});Conclusion
Authentication and authorization are critical components of secure web applications. Understanding the different approaches—session-based authentication, JWT tokens, and OAuth 2.0—allows you to choose the right solution for your specific use case.
Key Takeaways:
- Sessions work best for traditional web applications with server-side rendering
- JWT excels in stateless APIs, microservices, and mobile applications
- OAuth 2.0 is ideal for third-party integrations and delegated authorization
- Security is paramount: always hash passwords, use HTTPS, implement rate limiting, and follow best practices
- Choose wisely: Consider your application’s architecture, scalability needs, and security requirements
Remember that no single authentication method is perfect for all scenarios. The best approach depends on your application’s specific requirements, architecture, and security needs. By understanding the trade-offs and implementing security best practices, you can build robust authentication systems that protect your users and your application.
For more security best practices, check out our guide on web security best practices. If you’re working with APIs, our GraphQL vs REST API comparison can help you choose the right API architecture.
Next Steps:
- Implement authentication in your application using the method that best fits your needs
- Add proper security measures: rate limiting, CSRF protection, secure headers
- Test your implementation thoroughly, including edge cases and security scenarios
- Monitor authentication failures and implement logging for security analysis
- Keep dependencies updated and follow security advisories
Authentication is not a one-time implementation—it requires ongoing attention, monitoring, and updates to stay secure against evolving threats.