Authentication & Authorization Implementation Patterns
Build secure, scalable authentication and authorization systems using industry-standard patterns and modern best practices.
When to Use This Skill
-
Implementing user authentication systems
-
Securing REST or GraphQL APIs
-
Adding OAuth2/social login
-
Implementing role-based access control (RBAC)
-
Designing session management
-
Migrating authentication systems
-
Debugging auth issues
-
Implementing SSO or multi-tenancy
Core Concepts
- Authentication vs Authorization
Authentication (AuthN): Who are you?
-
Verifying identity (username/password, OAuth, biometrics)
-
Issuing credentials (sessions, tokens)
-
Managing login/logout
Authorization (AuthZ): What can you do?
-
Permission checking
-
Role-based access control (RBAC)
-
Resource ownership validation
-
Policy enforcement
- Authentication Strategies
Session-Based:
-
Server stores session state
-
Session ID in cookie
-
Traditional, simple, stateful
Token-Based (JWT):
-
Stateless, self-contained
-
Scales horizontally
-
Can store claims
OAuth2/OpenID Connect:
-
Delegate authentication
-
Social login (Google, GitHub)
-
Enterprise SSO
JWT Authentication
Pattern 1: JWT Implementation
// JWT structure: header.payload.signature import jwt from "jsonwebtoken"; import { Request, Response, NextFunction } from "express";
interface JWTPayload { userId: string; email: string; role: string; iat: number; exp: number; }
// Generate JWT function generateTokens(userId: string, email: string, role: string) { const accessToken = jwt.sign( { userId, email, role }, process.env.JWT_SECRET!, { expiresIn: "15m" }, // Short-lived );
const refreshToken = jwt.sign( { userId }, process.env.JWT_REFRESH_SECRET!, { expiresIn: "7d" }, // Long-lived );
return { accessToken, refreshToken }; }
// Verify JWT 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; } }
// Middleware 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; // Attach user to request next(); } catch (error) { return res.status(401).json({ error: "Invalid token" }); } }
// Usage app.get("/api/profile", authenticate, (req, res) => { res.json({ user: req.user }); });
Pattern 2: Refresh Token Flow
interface StoredRefreshToken { token: string; userId: string; expiresAt: Date; createdAt: Date; }
class RefreshTokenService { // Store refresh token in database 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, }); }
// Refresh access token async refreshAccessToken(refreshToken: string) { // Verify refresh token let payload; try { payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as { userId: string; }; } catch { throw new Error("Invalid refresh token"); }
// Check if token exists in database
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");
}
// Get user
const user = await db.users.findById(payload.userId);
if (!user) {
throw new Error("User not found");
}
// Generate new access token
const accessToken = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
process.env.JWT_SECRET!,
{ expiresIn: "15m" },
);
return { accessToken };
}
// Revoke refresh token (logout) async revokeRefreshToken(refreshToken: string) { await db.refreshTokens.deleteOne({ token: await hash(refreshToken), }); }
// Revoke all user tokens (logout all devices) async revokeAllUserTokens(userId: string) { await db.refreshTokens.deleteMany({ userId }); } }
// API endpoints app.post("/api/auth/refresh", async (req, res) => { const { refreshToken } = req.body; try { const { accessToken } = await refreshTokenService.refreshAccessToken(refreshToken); res.json({ accessToken }); } catch (error) { res.status(401).json({ error: "Invalid refresh token" }); } });
app.post("/api/auth/logout", authenticate, async (req, res) => { const { refreshToken } = req.body; await refreshTokenService.revokeRefreshToken(refreshToken); res.json({ message: "Logged out successfully" }); });
Session-Based Authentication
Pattern 1: Express Session
import session from "express-session"; import RedisStore from "connect-redis"; import { createClient } from "redis";
// Setup Redis for session storage 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", // HTTPS only httpOnly: true, // No JavaScript access maxAge: 24 * 60 * 60 * 1000, // 24 hours sameSite: "strict", // CSRF protection }, }), );
// Login app.post("/api/auth/login", async (req, res) => { const { email, password } = req.body;
const user = await db.users.findOne({ email }); if (!user || !(await verifyPassword(password, user.passwordHash))) { return res.status(401).json({ error: "Invalid credentials" }); }
// Store user in session req.session.userId = user.id; req.session.role = user.role;
res.json({ user: { id: user.id, email: user.email, role: user.role } }); });
// Session middleware function requireAuth(req: Request, res: Response, next: NextFunction) { if (!req.session.userId) { return res.status(401).json({ error: "Not authenticated" }); } next(); }
// Protected route app.get("/api/profile", requireAuth, async (req, res) => { const user = await db.users.findById(req.session.userId); res.json({ user }); });
// Logout app.post("/api/auth/logout", (req, res) => { req.session.destroy((err) => { if (err) { return res.status(500).json({ error: "Logout failed" }); } res.clearCookie("connect.sid"); res.json({ message: "Logged out successfully" }); }); });
OAuth2 / Social Login
Pattern 1: OAuth2 with Passport.js
import passport from "passport"; import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Strategy as GitHubStrategy } from "passport-github2";
// Google OAuth 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 { // Find or create user 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);
}
},
), );
// Routes app.get( "/api/auth/google", passport.authenticate("google", { scope: ["profile", "email"], }), );
app.get(
"/api/auth/google/callback",
passport.authenticate("google", { session: false }),
(req, res) => {
// Generate JWT
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
// Redirect to frontend with token
res.redirect(
${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken},
);
},
);
Authorization Patterns
Pattern 1: Role-Based Access Control (RBAC)
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 hasRole(userRole: Role, requiredRole: Role): boolean { return roleHierarchy[userRole].includes(requiredRole); }
// Middleware 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) => hasRole(req.user.role, role))) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
}; }
// Usage app.delete( "/api/users/:id", authenticate, requireRole(Role.ADMIN), async (req, res) => { // Only admins can delete users await db.users.delete(req.params.id); res.json({ message: "User deleted" }); }, );
Pattern 2: Permission-Based Access Control
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 hasPermission(userRole: Role, permission: Permission): boolean { return rolePermissions[userRole]?.includes(permission) ?? false; }
function requirePermission(...permissions: Permission[]) { return (req: Request, res: Response, next: NextFunction) => { if (!req.user) { return res.status(401).json({ error: "Not authenticated" }); }
const hasAllPermissions = permissions.every((permission) =>
hasPermission(req.user.role, permission),
);
if (!hasAllPermissions) {
return res.status(403).json({ error: "Insufficient permissions" });
}
next();
}; }
// Usage app.get( "/api/users", authenticate, requirePermission(Permission.READ_USERS), async (req, res) => { const users = await db.users.findAll(); res.json({ users }); }, );
Pattern 3: Resource Ownership
// Check if user owns resource 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" }); }
const resourceId = req.params[resourceIdParam];
// Admins can access anything
if (req.user.role === Role.ADMIN) {
return next();
}
// Check ownership
let resource;
if (resourceType === "post") {
resource = await db.posts.findById(resourceId);
} else if (resourceType === "comment") {
resource = 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();
}; }
// Usage app.put( "/api/posts/:id", authenticate, requireOwnership("post"), async (req, res) => { // User can only update their own posts const post = await db.posts.update(req.params.id, req.body); res.json({ post }); }, );
Security Best Practices
Pattern 1: Password Security
import bcrypt from "bcrypt"; import { z } from "zod";
// Password validation schema const passwordSchema = z .string() .min(12, "Password must be at least 12 characters") .regex(/[A-Z]/, "Password must contain uppercase letter") .regex(/[a-z]/, "Password must contain lowercase letter") .regex(/[0-9]/, "Password must contain number") .regex(/[^A-Za-z0-9]/, "Password must contain special character");
// Hash password async function hashPassword(password: string): Promise<string> { const saltRounds = 12; // 2^12 iterations return bcrypt.hash(password, saltRounds); }
// Verify password async function verifyPassword( password: string, hash: string, ): Promise<boolean> { return bcrypt.compare(password, hash); }
// Registration with password validation app.post("/api/auth/register", async (req, res) => { try { const { email, password } = req.body;
// Validate password
passwordSchema.parse(password);
// Check if user exists
const existingUser = await db.users.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: "Email already registered" });
}
// Hash password
const passwordHash = await hashPassword(password);
// Create user
const user = await db.users.create({
email,
passwordHash,
});
// Generate tokens
const tokens = generateTokens(user.id, user.email, user.role);
res.status(201).json({
user: { id: user.id, email: user.email },
...tokens,
});
} catch (error) { if (error instanceof z.ZodError) { return res.status(400).json({ error: error.errors[0].message }); } res.status(500).json({ error: "Registration failed" }); } });
Pattern 2: Rate Limiting
import rateLimit from "express-rate-limit"; import RedisStore from "rate-limit-redis";
// Login rate limiter const loginLimiter = rateLimit({ store: new RedisStore({ client: redisClient }), windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts message: "Too many login attempts, please try again later", standardHeaders: true, legacyHeaders: false, });
// API rate limiter const apiLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minute max: 100, // 100 requests per minute standardHeaders: true, });
// Apply to routes app.post("/api/auth/login", loginLimiter, async (req, res) => { // Login logic });
app.use("/api/", apiLimiter);
Best Practices
-
Never Store Plain Passwords: Always hash with bcrypt/argon2
-
Use HTTPS: Encrypt data in transit
-
Short-Lived Access Tokens: 15-30 minutes max
-
Secure Cookies: httpOnly, secure, sameSite flags
-
Validate All Input: Email format, password strength
-
Rate Limit Auth Endpoints: Prevent brute force attacks
-
Implement CSRF Protection: For session-based auth
-
Rotate Secrets Regularly: JWT secrets, session secrets
-
Log Security Events: Login attempts, failed auth
-
Use MFA When Possible: Extra security layer
Common Pitfalls
-
Weak Passwords: Enforce strong password policies
-
JWT in localStorage: Vulnerable to XSS, use httpOnly cookies
-
No Token Expiration: Tokens should expire
-
Client-Side Auth Checks Only: Always validate server-side
-
Insecure Password Reset: Use secure tokens with expiration
-
No Rate Limiting: Vulnerable to brute force
-
Trusting Client Data: Always validate on server