635 lines
17 KiB
Markdown
635 lines
17 KiB
Markdown
---
|
|
name: auth-implementation-patterns
|
|
description: Master authentication and authorization patterns including JWT, OAuth2, session management, and RBAC to build secure, scalable access control systems. Use when implementing auth systems, securing APIs, or debugging security issues.
|
|
---
|
|
|
|
# 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
|
|
|
|
### 1. 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
|
|
|
|
### 2. 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```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 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
|
|
|
|
```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 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
1. **Never Store Plain Passwords**: Always hash with bcrypt/argon2
|
|
2. **Use HTTPS**: Encrypt data in transit
|
|
3. **Short-Lived Access Tokens**: 15-30 minutes max
|
|
4. **Secure Cookies**: httpOnly, secure, sameSite flags
|
|
5. **Validate All Input**: Email format, password strength
|
|
6. **Rate Limit Auth Endpoints**: Prevent brute force attacks
|
|
7. **Implement CSRF Protection**: For session-based auth
|
|
8. **Rotate Secrets Regularly**: JWT secrets, session secrets
|
|
9. **Log Security Events**: Login attempts, failed auth
|
|
10. **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
|
|
|
|
## Resources
|
|
|
|
- **references/jwt-best-practices.md**: JWT implementation guide
|
|
- **references/oauth2-flows.md**: OAuth2 flow diagrams and examples
|
|
- **references/session-security.md**: Secure session management
|
|
- **assets/auth-security-checklist.md**: Security review checklist
|
|
- **assets/password-policy-template.md**: Password requirements template
|
|
- **scripts/token-validator.ts**: JWT validation utility
|