Authentication & Authorization Implementation Patterns
JWT Authentication
JWT Implementation
typescript
import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express';
interface JWTPayload {
userId: string;
email: string;
role: string;
iat: number;
exp: number;
}
function generateTokens(userId: string, email: string, role: string) {
const accessToken = jwt.sign(
{ userId, email, role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
function verifyToken(token: string): JWTPayload {
try {
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
throw new Error('Token expired');
}
if (error instanceof jwt.JsonWebTokenError) {
throw new Error('Invalid token');
}
throw error;
}
}
function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
Refresh Token Flow
typescript
class RefreshTokenService {
async storeRefreshToken(userId: string, refreshToken: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
await db.refreshTokens.create({
token: await hash(refreshToken), // Hash before storing
userId,
expiresAt,
});
}
async refreshAccessToken(refreshToken: string) {
let payload;
try {
payload = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET!
) as { userId: string };
} catch {
throw new Error('Invalid refresh token');
}
const storedToken = await db.refreshTokens.findOne({
where: {
token: await hash(refreshToken),
userId: payload.userId,
expiresAt: { $gt: new Date() },
},
});
if (!storedToken) throw new Error('Refresh token not found or expired');
const user = await db.users.findById(payload.userId);
if (!user) throw new Error('User not found');
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
return { accessToken };
}
async revokeRefreshToken(refreshToken: string) {
await db.refreshTokens.deleteOne({ token: await hash(refreshToken) });
}
async revokeAllUserTokens(userId: string) {
await db.refreshTokens.deleteMany({ userId });
}
}
Session-Based Authentication
typescript
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(
session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
sameSite: 'strict',
},
})
);
OAuth2 with Passport.js
typescript
import passport from 'passport';
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
callbackURL: '/api/auth/google/callback',
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await db.users.findOne({ googleId: profile.id });
if (!user) {
user = await db.users.create({
googleId: profile.id,
email: profile.emails?.[0]?.value,
name: profile.displayName,
avatar: profile.photos?.[0]?.value,
});
}
return done(null, user);
} catch (error) {
return done(error, undefined);
}
}
)
);
app.get('/api/auth/google', passport.authenticate('google', {
scope: ['profile', 'email'],
}));
app.get(
'/api/auth/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`);
}
);
Authorization Patterns
Role-Based Access Control (RBAC)
typescript
enum Role {
USER = 'user',
MODERATOR = 'moderator',
ADMIN = 'admin',
}
const roleHierarchy: Record<Role, Role[]> = {
[Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER],
[Role.MODERATOR]: [Role.MODERATOR, Role.USER],
[Role.USER]: [Role.USER],
};
function requireRole(...roles: Role[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
if (!roles.some(role => roleHierarchy[req.user.role].includes(role))) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
Permission-Based Access Control
typescript
enum Permission {
READ_USERS = 'read:users',
WRITE_USERS = 'write:users',
DELETE_USERS = 'delete:users',
READ_POSTS = 'read:posts',
WRITE_POSTS = 'write:posts',
}
const rolePermissions: Record<Role, Permission[]> = {
[Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS],
[Role.MODERATOR]: [Permission.READ_POSTS, Permission.WRITE_POSTS, Permission.READ_USERS],
[Role.ADMIN]: Object.values(Permission),
};
function requirePermission(...permissions: Permission[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
const hasAll = permissions.every(p =>
rolePermissions[req.user.role]?.includes(p) ?? false
);
if (!hasAll) return res.status(403).json({ error: 'Insufficient permissions' });
next();
};
}
Resource Ownership
typescript
async function requireOwnership(resourceType: 'post' | 'comment', resourceIdParam: string = 'id') {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) return res.status(401).json({ error: 'Not authenticated' });
if (req.user.role === Role.ADMIN) return next();
const resourceId = req.params[resourceIdParam];
const resource = resourceType === 'post'
? await db.posts.findById(resourceId)
: await db.comments.findById(resourceId);
if (!resource) return res.status(404).json({ error: 'Resource not found' });
if (resource.userId !== req.user.userId) return res.status(403).json({ error: 'Not authorized' });
next();
};
}
Password Security
typescript
import bcrypt from 'bcrypt';
import { z } from 'zod';
const passwordSchema = z.string()
.min(12, 'Password must be at least 12 characters')
.regex(/[A-Z]/, 'Must contain uppercase')
.regex(/[a-z]/, 'Must contain lowercase')
.regex(/[0-9]/, 'Must contain number')
.regex(/[^A-Za-z0-9]/, 'Must contain special character');
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
Rate Limiting
typescript
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
const loginLimiter = rateLimit({
store: new RedisStore({ client: redisClient }),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5,
message: 'Too many login attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
});
app.post('/api/auth/login', loginLimiter, async (req, res) => { /* ... */ });
app.use('/api/', apiLimiter);
Common Pitfalls
- •JWT in localStorage: Vulnerable to XSS, use httpOnly cookies
- •No Token Expiration: Tokens should expire (15-30min access, 7d refresh)
- •Client-Side Auth Only: Always validate server-side
- •Insecure Password Reset: Use secure tokens with expiration
- •No Rate Limiting: Vulnerable to brute force