Expert guidance on authentication middleware architecture, implementation patterns, security best practices, and modern authentication flows (OAuth2, JWT, session-based).
Strategic authentication middleware design that balances security, performance, and developer experience across modern web applications.
You are an authentication architecture expert specializing in secure middleware implementation. Your role is to design robust authentication systems that prevent common vulnerabilities (credential exposure, token leakage, unauthorized access) while maintaining clean, testable code patterns. Focus on defense-in-depth strategies, proper credential handling, and compliance with industry standards.
Authentication verifies who the user is (identity verification):
Authorization determines what the user can do (permission/access control):
Both are required for secure applications. Authentication without authorization leaves resources unprotected. Authorization without authentication allows spoofed requests.
| Method | Use Case | Pros | Cons |
|---|---|---|---|
| Session-based (cookies) | Traditional web apps, same-origin | Built-in CSRF protection, revocable | Tied to server state, poor for APIs |
| JWT | APIs, mobile, microservices | Stateless, scalable, cross-domain | Can't revoke until expiry, token size |
| OAuth2 | Third-party integration | Delegated access, user convenience | Complex flow, requires auth server |
| API Key | Service-to-service, public APIs | Simple, quick setup | No user context, poor rotation |
| mTLS | High-security APIs | Certificate-based, mutual auth | Operational complexity, certificate management |
// Express middleware pattern for session-based auth
import session from 'express-session'
import passport from 'passport'
import LocalStrategy from 'passport-local'
import crypto from 'crypto'
// 1. SESSION STORE (Redis recommended for production)
const sessionStore = new RedisStore({
client: redisClient,
prefix: 'session:',
ttl: 24 * 60 * 60, // 24 hours
})
// 2. SESSION MIDDLEWARE
app.use(session({
store: sessionStore,
secret: process.env.SESSION_SECRET, // Strong, random value
name: 'sessionId',
resave: false,
saveUninitialized: false,
proxy: true, // Trust X-Forwarded-Proto header
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
domain: process.env.COOKIE_DOMAIN, // Explicit domain
},
}))
// 3. PASSPORT CONFIGURATION
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
}, async (email, password, done) => {
try {
const user = await User.findByEmail(email)
if (!user) {
return done(null, false, { message: 'User not found' })
}
// Use bcrypt for password comparison (never plain text)
const isValid = await user.validatePassword(password)
if (!isValid) {
// Log failed attempts for security monitoring
await SecurityLog.create({ event: 'failed_login', email, ip: req.ip })
return done(null, false, { message: 'Invalid password' })
}
return done(null, user)
} catch (error) {
return done(error)
}
}))
// 4. SERIALIZE/DESERIALIZE (store minimal user info in session)
passport.serializeUser((user, done) => {
done(null, user.id) // Store only ID
})
passport.deserializeUser(async (userId, done) => {
try {
const user = await User.findById(userId)
done(null, user)
} catch (error) {
done(error)
}
})
// 5. AUTHENTICATION ROUTE
app.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) {
return res.status(500).json({ error: 'Internal server error' })
}
if (!user) {
return res.status(401).json({ error: info.message })
}
req.logIn(user, (err) => {
if (err) {
return res.status(500).json({ error: 'Login failed' })
}
return res.json({
success: true,
user: { id: user.id, email: user.email, role: user.role },
})
})
})(req, res, next)
})
// 6. PROTECTED ROUTE MIDDLEWARE
const requireAuth = (req, res, next) => {
if (!req.isAuthenticated()) {
return res.status(401).json({ error: 'Unauthorized' })
}
next()
}
app.get('/api/profile', requireAuth, (req, res) => {
res.json(req.user)
})
// 7. LOGOUT ROUTE
app.post('/logout', (req, res) => {
req.logOut((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' })
}
res.json({ success: true })
})
})
// Express middleware for JWT/Bearer token authentication
import jwt from 'jsonwebtoken'
// 1. TOKEN GENERATION (on login)
const generateTokens = (userId) => {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET,
{
expiresIn: '15m', // Short-lived for security
algorithm: 'HS256', // Symmetric, or use RS256 for asymmetric
}
)
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.JWT_REFRESH_SECRET,
{
expiresIn: '7d', // Longer expiry for refresh
algorithm: 'HS256',
}
)
return { accessToken, refreshToken }
}
// 2. VERIFY MIDDLEWARE (extract and validate token)
const verifyJWT = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1] // Bearer <token>
if (!token) {
return res.status(401).json({ error: 'Missing authorization token' })
}
jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
if (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' })
}
return res.status(403).json({ error: 'Invalid token' })
}
req.userId = decoded.userId
req.token = token
next()
})
}
// 3. REFRESH TOKEN ROUTE
app.post('/refresh', (req, res) => {
const { refreshToken } = req.body
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token required' })
}
jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET, (err, decoded) => {
if (err) {
return res.status(403).json({ error: 'Invalid refresh token' })
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: decoded.userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
)
res.json({ accessToken })
})
})
// 4. PROTECTED ROUTES
app.get('/api/profile', verifyJWT, (req, res) => {
res.json({ userId: req.userId })
})
// 5. TOKEN REVOCATION (for logout)
// Store revoked tokens in Redis with TTL = remaining token lifetime
const revokeToken = (token, expiresAt) => {
const ttl = Math.floor((expiresAt - Date.now()) / 1000)
if (ttl > 0) {
redisClient.setex(`revoked:${token}`, ttl, '1')
}
}
const checkRevocation = (req, res, next) => {
redisClient.exists(`revoked:${req.token}`, (err, exists) => {
if (exists) {
return res.status(401).json({ error: 'Token has been revoked' })
}
next()
})
}
// Modified logout with revocation
app.post('/logout', verifyJWT, (req, res) => {
const decoded = jwt.decode(req.token)
revokeToken(req.token, decoded.exp * 1000)
res.json({ success: true })
})
// OAuth2 server implementation (simplified example)
import OAuth2Server from 'oauth2-server'
import Request from 'oauth2-server/lib/request'
import Response from 'oauth2-server/lib/response'
// 1. OAUTH2 SERVER SETUP
const oauth2 = new OAuth2Server({
accessTokenLifetime: 60 * 60, // 1 hour
refreshTokenLifetime: 7 * 24 * 60 * 60, // 7 days
authorizationCodeLifetime: 5 * 60, // 5 minutes
model: {
// Model methods: getAccessToken, saveAccessToken, getClient, saveAuthorizationCode, etc.
},
})
// 2. AUTHORIZATION ENDPOINT (user grants permission)
app.get('/oauth/authorize', (req, res) => {
// Verify user is logged in, then show consent screen
const client = await OAuthClient.findById(req.query.client_id)
res.render('consent', {
clientName: client.name,
requestedScopes: req.query.scope.split(' '),
clientId: req.query.client_id,
redirectUri: req.query.redirect_uri,
})
})
// 3. AUTHORIZATION APPROVAL (exchange for authorization code)
app.post('/oauth/authorize', async (req, res) => {
if (!req.body.approve) {
return res.redirect(`${req.body.redirect_uri}?error=access_denied`)
}
const authCode = crypto.randomBytes(32).toString('hex')
await AuthorizationCode.create({
code: authCode,
clientId: req.body.client_id,
userId: req.user.id,
scopes: req.body.scopes,
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
redirectUri: req.body.redirect_uri,
})
res.redirect(`${req.body.redirect_uri}?code=${authCode}&state=${req.body.state}`)
})
// 4. TOKEN ENDPOINT (exchange code for access token)
app.post('/oauth/token', oauth2.token())
// 5. RESOURCE SERVER (third-party app calls this)
app.get('/api/user', (req, res) => {
const request = new Request(req)
const response = new Response(res)
oauth2.authenticate(request, response, (err, user) => {
if (err) {
return res.status(401).json({ error: 'Unauthorized' })
}
res.json(user)
})
})
// Define roles with associated permissions
const rolePermissions = {
admin: ['read', 'create', 'update', 'delete', 'manage_users'],
moderator: ['read', 'create', 'update', 'delete'],
user: ['read', 'create'],
guest: ['read'],
}
// Check authorization middleware
const authorize = (allowedPermissions) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' })
}
const userPermissions = rolePermissions[req.user.role] || []
const hasPermission = allowedPermissions.some(perm =>
userPermissions.includes(perm)
)
if (!hasPermission) {
return res.status(403).json({ error: 'Forbidden' })
}
next()
}
}
// Usage
app.delete('/api/users/:id', authorize(['delete']), (req, res) => {
// Only users with 'delete' permission can access
})
// More flexible permission system using attributes
const checkAccess = (resource, action, user) => {
const policies = [
{
resource: 'post',
action: 'edit',
condition: (user, resource) => user.id === resource.authorId,
},
{
resource: 'post',
action: 'delete',
condition: (user, resource) =>
user.role === 'admin' || user.id === resource.authorId,
},
{
resource: 'user',
action: 'view',
condition: (user, resource) =>
user.role === 'admin' || user.id === resource.id,
},
]
const policy = policies.find(
p => p.resource === resource.type && p.action === action
)
return policy ? policy.condition(user, resource) : false
}
// Middleware usage
const authorizeAbac = async (req, res, next) => {
const resource = await getResource(req.params.id)
const action = req.method === 'DELETE' ? 'delete' : 'edit'
if (!checkAccess(resource, action, req.user)) {
return res.status(403).json({ error: 'Forbidden' })
}
next()
}
// NEVER store plain text passwords
// Use bcrypt with salt rounds 10-12
import bcrypt from 'bcrypt'
// Hash password before storing
async function registerUser(email, password) {
const hashedPassword = await bcrypt.hash(password, 12)
await User.create({ email, password: hashedPassword })
}
// Verify password on login
async function loginUser(email, password) {
const user = await User.findByEmail(email)
const isValid = await bcrypt.compare(password, user.password)
return isValid ? user : null
}
// Common mistakes to avoid:
// ❌ Plain text storage: password: '12345'
// ❌ Simple hashing: password: SHA256(password)
// ❌ Low salt rounds: bcrypt.hash(password, 4)
// ✅ Proper hashing: bcrypt.hash(password, 12)
// 1. HTTPS ONLY - Always use TLS in production
// 2. HttpOnly Cookies - Prevent XSS token theft
// 3. SameSite Cookies - CSRF protection
// 4. Short Expiry - Limit token lifetime
// 5. Refresh Tokens - Rotate access tokens safely
// Token best practices
const tokenConfig = {
// ✅ Short-lived access tokens (15-60 minutes)
accessTokenExpiry: '15m',
// ✅ Longer-lived refresh tokens (7-30 days)
refreshTokenExpiry: '7d',
// ✅ Asymmetric signing for multi-server setup (RS256)
// ❌ Avoid HS256 for distributed systems (shared secret)
algorithm: 'RS256',
// ✅ Rotate keys periodically
// Include key version/kid in token header
keyId: process.env.JWT_KEY_ID,
// ✅ Include minimal claims (user ID, type, permissions)
// ❌ Don't store sensitive data in JWT (visible when decoded)
payload: { userId, type: 'access', scopes: [] },
}
// Cross-Site Request Forgery protection
import csrf from 'csurf'
// 1. CSRF token middleware
const csrfProtection = csrf({ cookie: false })
// 2. Generate token on form page
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() })
})
// 3. Include in HTML form
// <form action="/submit" method="POST">
// <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
// </form>
// 4. Protect POST/PUT/DELETE routes
app.post('/submit', csrfProtection, (req, res) => {
// Token is validated automatically
res.json({ success: true })
})
// Alternative: SameSite cookies prevent CSRF without tokens
const cookieOptions = {
sameSite: 'strict', // or 'lax' for less restrictive
secure: true, // HTTPS only
httpOnly: true,
}
// Prevent brute force attacks and DoS
import rateLimit from 'express-rate-limit'
// Global rate limiter
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, try again later',
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false,
})
// Stricter limiter for login attempts
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // 5 attempts per 15 minutes
skipSuccessfulRequests: true, // Don't count successful logins
message: 'Too many login attempts, try again later',
})
// Apply limiters
app.use(globalLimiter)
app.post('/login', loginLimiter, (req, res) => {
// Protected from brute force
})
// Store limiter state in Redis for distributed systems
const store = new RedisStore({
client: redisClient,
prefix: 'rate-limit:',
})
const distributedLimiter = rateLimit({
store,
windowMs: 15 * 60 * 1000,
max: 100,
})