- Added request/response flow diagrams to api-design and api-gateway-advanced skills for better visualization of processes. - Introduced configuration loading flow in configuration-management skill to clarify the configuration process. - Included error propagation flow in error-handling-patterns skill to illustrate error handling across layers. - Enhanced various skills with additional diagrams to improve understanding of complex concepts. These updates aim to provide clearer guidance and improve the overall documentation experience for developers.
26 KiB
26 KiB
name, description
| name | description |
|---|---|
| security | Security best practices and patterns for GoodGo microservices platform. Use when implementing authentication, authorization, data protection, input validation, rate limiting, secrets management, or security testing across all services. |
Security Patterns for GoodGo Microservices
When to Use This Skill
Use this skill when:
- Implementing authentication and authorization in any service
- Protecting sensitive data (PII, credentials, tokens)
- Validating user inputs and file uploads
- Implementing rate limiting and DDoS protection
- Setting up audit logging and security monitoring
- Encrypting data at rest and in transit
- Managing secrets and credentials
- Implementing security testing
- Handling security incidents
- Designing secure API endpoints
Core Security Principles
- Defense in Depth: Multiple layers of security controls
- Least Privilege: Grant minimum required permissions
- Fail Secure: Default to deny access
- Separation of Duties: Critical operations require multiple approvals
- Audit Everything: Log all security-relevant events
- Encrypt Sensitive Data: PII, tokens, credentials must be encrypted
- Validate All Inputs: Never trust user input
- Principle of Least Exposure: Minimize attack surface
- Secure by Default: Security built-in, not bolted on
- Assume Breach: Design for detection and response
Authentication & Authorization
JWT Token Validation
The following diagram illustrates the authentication flow when a client makes a request with a JWT token:
sequenceDiagram
participant Client
participant Middleware as Auth Middleware
participant JWTService as JWT Service
participant Request as Express Request
Client->>Middleware: HTTP Request with Token
Middleware->>Middleware: Extract token from<br/>Authorization header or cookie
alt Token not found
Middleware->>Client: 401 Unauthorized<br/>(AUTH_REQUIRED)
else Token found
Middleware->>JWTService: verifyAccessToken(token)
alt Token invalid or expired
JWTService->>Middleware: Verification failed
Middleware->>Client: 401 Unauthorized<br/>(INVALID_TOKEN)
else Token valid
JWTService->>Middleware: Payload (sub, email, roles, permissions)
Middleware->>Request: Attach user to req.user
Middleware->>Client: Continue to next middleware
end
end
// src/middlewares/auth.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { jwtService } from '@goodgo/auth-sdk';
import { logger } from '@goodgo/logger';
export const authenticate = () => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
// Extract token from Authorization header or cookie
let token: string | null = null;
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
token = authHeader.substring(7);
} else if (req.cookies?.access_token) {
token = req.cookies.access_token;
}
if (!token) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_REQUIRED', message: 'Authentication required' }
});
}
// Verify token
const payload = await jwtService.verifyAccessToken(token);
// Attach user to request
req.user = {
id: payload.sub,
userId: payload.sub,
email: payload.email,
roles: payload.roles || [],
permissions: payload.permissions || []
};
next();
} catch (error) {
logger.warn('Authentication failed', { error: error.message });
return res.status(401).json({
success: false,
error: { code: 'INVALID_TOKEN', message: 'Invalid or expired token' }
});
}
};
};
Role-Based Authorization
The authorization decision flow determines whether a user has the required permissions to access a resource:
flowchart TD
Start([Request Received]) --> CheckAuth{User<br/>Authenticated?}
CheckAuth -->|No| Return401[Return 401<br/>AUTH_REQUIRED]
CheckAuth -->|Yes| CheckType{Authorization<br/>Type?}
CheckType -->|Role-Based| CheckRole{User has<br/>Required Role?}
CheckType -->|Permission-Based| CheckPermission{User has<br/>Resource:Action<br/>Permission?}
CheckType -->|Ownership| CheckOwnership{Resource ID<br/>matches User ID?}
CheckRole -->|No| LogDenial[Log Permission Denied<br/>with user roles]
CheckPermission -->|No| LogDenial
CheckOwnership -->|No| LogDenial
LogDenial --> Return403[Return 403<br/>FORBIDDEN]
CheckRole -->|Yes| Allow[Allow Request<br/>Continue to Handler]
CheckPermission -->|Yes| Allow
CheckOwnership -->|Yes| Allow
Return401 --> End([End])
Return403 --> End
Allow --> End
style CheckAuth fill:#e1f5ff
style CheckType fill:#e1f5ff
style Return401 fill:#ffebee
style Return403 fill:#ffebee
style Allow fill:#e8f5e9
// src/middlewares/rbac.middleware.ts
export const requireRole = (...allowedRoles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_REQUIRED', message: 'Authentication required' }
});
}
const userRoles = req.user.roles || [];
const hasRole = userRoles.some(role => allowedRoles.includes(role));
if (!hasRole) {
logger.warn('Access denied - insufficient role', {
userId: req.user.id,
userRoles,
requiredRoles: allowedRoles
});
return res.status(403).json({
success: false,
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' }
});
}
next();
};
};
// Permission-based authorization
export const requirePermission = (resource: string, action: string) => {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: { code: 'AUTH_REQUIRED', message: 'Authentication required' }
});
}
const permission = `${resource}:${action}`;
const hasPermission = req.user.permissions?.includes(permission);
if (!hasPermission) {
logger.warn('Access denied - insufficient permission', {
userId: req.user.id,
required: permission
});
return res.status(403).json({
success: false,
error: { code: 'FORBIDDEN', message: 'Insufficient permissions' }
});
}
next();
};
};
// Usage in routes
router.post(
'/api/v1/users',
authenticate(),
requirePermission('users', 'create'),
userController.create
);
Resource Ownership Validation
// Ensure users can only access their own resources
export const requireOwnership = (resourceIdParam: string = 'id') => {
return (req: Request, res: Response, next: NextFunction) => {
const resourceId = req.params[resourceIdParam];
const userId = req.user?.id;
if (resourceId !== userId) {
logger.warn('Access denied - resource ownership mismatch', {
userId,
resourceId
});
return res.status(403).json({
success: false,
error: { code: 'FORBIDDEN', message: 'Access denied' }
});
}
next();
};
};
Data Protection
Encryption Service
The encryption and decryption flow for protecting sensitive data at rest:
sequenceDiagram
participant Service
participant EncryptionService
participant Crypto as Node.js Crypto
participant Database
Note over Service,Database: Encryption Flow
Service->>EncryptionService: encrypt(plaintext)
EncryptionService->>Crypto: Generate random IV<br/>(16 bytes)
EncryptionService->>Crypto: Create cipher<br/>(AES-256-GCM)
EncryptionService->>Crypto: Encrypt plaintext
Crypto->>EncryptionService: Encrypted data + Auth Tag
EncryptionService->>Service: Format: iv:tag:ciphertext
Service->>Database: Store encrypted data
Note over Service,Database: Decryption Flow
Service->>Database: Retrieve encrypted data
Database->>Service: iv:tag:ciphertext
Service->>EncryptionService: decrypt(encryptedText)
EncryptionService->>EncryptionService: Split iv, tag, ciphertext
EncryptionService->>Crypto: Create decipher<br/>(AES-256-GCM)
EncryptionService->>Crypto: Set auth tag
EncryptionService->>Crypto: Decrypt ciphertext
Crypto->>EncryptionService: Plaintext
EncryptionService->>Service: Return plaintext
// src/core/security/encryption.service.ts
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 16;
const TAG_LENGTH = 16;
export class EncryptionService {
private getKey(): Buffer {
const secret = process.env.ENCRYPTION_KEY;
if (!secret || secret.length < 32) {
throw new Error('ENCRYPTION_KEY must be at least 32 characters');
}
return crypto.scryptSync(secret, 'salt', 32);
}
encrypt(text: string): string {
const key = this.getKey();
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const tag = cipher.getAuthTag();
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted}`;
}
decrypt(encryptedText: string): string {
const [ivHex, tagHex, encrypted] = encryptedText.split(':');
const iv = Buffer.from(ivHex, 'hex');
const tag = Buffer.from(tagHex, 'hex');
const key = this.getKey();
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(tag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
// Usage: Encrypt PII before storing
const encryption = new EncryptionService();
const encryptedPhone = encryption.encrypt(user.phone);
Password Hashing
The password hashing and verification flow:
sequenceDiagram
participant User
participant Service
participant PasswordService
participant Bcrypt
participant Database
Note over User,Database: Registration/Password Change
User->>Service: Submit password
Service->>PasswordService: hash(password)
PasswordService->>Bcrypt: bcrypt.hash(password, 12)
Bcrypt->>Bcrypt: Generate salt
Bcrypt->>Bcrypt: Hash with cost factor 12
Bcrypt->>PasswordService: Hashed password
PasswordService->>Service: Return hash
Service->>Database: Store passwordHash
Service->>Service: Sanitize password<br/>before logging
Note over User,Database: Login Verification
User->>Service: Submit credentials
Service->>Database: Fetch user by email
Database->>Service: User with passwordHash
Service->>PasswordService: verify(password, hash)
PasswordService->>Bcrypt: bcrypt.compare(password, hash)
Bcrypt->>PasswordService: Boolean result
PasswordService->>Service: Return verification result
alt Password matches
Service->>User: Authentication success
else Password mismatch
Service->>User: Invalid credentials<br/>(generic error)
end
// Always use bcrypt with appropriate cost factor
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12; // Production: 12, Development: 10
export class PasswordService {
async hash(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async verify(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
// Never log passwords
sanitizeForLogging(data: any): any {
const sanitized = { ...data };
if (sanitized.password) sanitized.password = '[REDACTED]';
if (sanitized.passwordHash) sanitized.passwordHash = '[REDACTED]';
return sanitized;
}
}
Token Hashing
// Hash tokens before storing in database
import crypto from 'crypto';
export class TokenService {
hashToken(token: string): string {
const salt = process.env.TOKEN_SALT || 'default-salt-change-in-production';
return crypto
.createHash('sha256')
.update(token + salt)
.digest('hex');
}
generateSecureToken(length: number = 32): string {
return crypto.randomBytes(length).toString('hex');
}
}
Input Validation
Zod Schema Validation
// Always validate inputs with Zod
import { z } from 'zod';
// DTO with validation
export const CreateUserDto = z.object({
email: z.string().email('Invalid email format'),
password: z.string()
.min(8, 'Password must be at least 8 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'),
phone: z.string()
.regex(/^\+[1-9]\d{1,14}$/, 'Invalid phone format (E.164)')
.optional(),
name: z.string().min(1).max(255)
});
// In controller
export class UserController {
async create(req: Request, res: Response) {
try {
const dto = CreateUserDto.parse(req.body);
const user = await this.service.create(dto);
res.status(201).json({ success: true, data: user });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: error.errors
}
});
}
throw error;
}
}
}
File Upload Validation
// Validate file uploads
import fileType from 'file-type';
export class FileValidationService {
private readonly MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
private readonly ALLOWED_TYPES = ['image/jpeg', 'image/png', 'application/pdf'];
async validateFile(file: Express.Multer.File): Promise<void> {
// Size check
if (file.size > this.MAX_FILE_SIZE) {
throw new HttpError(400, 'FILE_TOO_LARGE', 'File exceeds maximum size');
}
// Type check
if (!this.ALLOWED_TYPES.includes(file.mimetype)) {
throw new HttpError(400, 'INVALID_FILE_TYPE', 'File type not allowed');
}
// Content validation (prevent MIME type spoofing)
const type = await fileType.fromBuffer(file.buffer);
if (!type || !this.ALLOWED_TYPES.includes(type.mime)) {
throw new HttpError(400, 'INVALID_FILE_CONTENT', 'File content mismatch');
}
// TODO: Add virus scanning for production
}
}
SQL Injection Prevention
// Always use Prisma parameterized queries (automatic)
// Never use string concatenation for queries
// ❌ BAD - Never do this
const query = `SELECT * FROM users WHERE email = '${email}'`;
// ✅ GOOD - Use Prisma
const user = await prisma.user.findUnique({
where: { email }
});
// ✅ GOOD - For dynamic queries
const where: any = {};
if (email) where.email = email;
if (status) where.status = status;
const users = await prisma.user.findMany({ where });
Rate Limiting
// Implement rate limiting for all endpoints
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
// Standard rate limit
export const standardLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:standard:'
}),
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false
});
// Strict rate limit for sensitive operations
export const strictLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:strict:'
}),
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
message: 'Rate limit exceeded for this operation'
});
// Login-specific rate limit
export const loginLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: 'rl:login:'
}),
windowMs: 15 * 60 * 1000,
max: 5, // 5 login attempts per 15 minutes
skipSuccessfulRequests: true,
message: 'Too many login attempts, please try again later'
});
// Usage
router.post('/api/v1/auth/login', loginLimiter, authController.login);
router.post('/api/v1/users', authenticate(), strictLimiter, userController.create);
Error Handling Security
// Sanitize error messages to prevent information disclosure
export class SecureErrorHandler {
handleError(error: Error, req: Request, res: Response) {
const isDev = process.env.NODE_ENV === 'development';
const isProd = process.env.NODE_ENV === 'production';
// Log full error internally
logger.error('Request error', {
error: error.message,
stack: error.stack,
path: req.path,
method: req.method,
userId: req.user?.id
});
// Don't expose user existence
if (error.message.includes('user not found') ||
error.message.includes('invalid credentials')) {
return res.status(401).json({
success: false,
error: {
code: 'INVALID_CREDENTIALS',
message: 'Invalid email or password'
}
});
}
// Validation errors - safe to expose
if (error instanceof z.ZodError) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: error.errors
}
});
}
// Generic errors for production
if (isProd) {
return res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An error occurred. Please try again later.'
}
});
}
// Detailed errors only in development
return res.status(500).json({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: error.message,
stack: isDev ? error.stack : undefined
}
});
}
}
Secrets Management
// Never hardcode secrets
// Always use environment variables with validation
import { z } from 'zod';
const secretsSchema = z.object({
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
JWT_REFRESH_SECRET: z.string().min(32),
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
ENCRYPTION_KEY: z.string().min(32).optional()
});
export const secrets = secretsSchema.parse(process.env);
// For production, use secret management:
// - AWS Secrets Manager
// - HashiCorp Vault
// - Kubernetes Secrets
// - Azure Key Vault
// Rotate secrets regularly (quarterly recommended)
Audit Logging
// Log all security-relevant events
export class AuditService {
async logSecurityEvent(
event: string,
userId: string | null,
details: Record<string, any>,
req?: Request
) {
await this.prisma.auditLog.create({
data: {
event,
userId,
type: 'SECURITY',
details: this.sanitizeDetails(details),
ipAddress: req?.ip || details.ipAddress,
userAgent: req?.get('user-agent'),
timestamp: new Date()
}
});
}
// Sanitize PII from logs
private sanitizeDetails(details: Record<string, any>): Record<string, any> {
const sensitive = ['password', 'token', 'secret', 'ssn', 'creditCard'];
const sanitized = { ...details };
for (const key of sensitive) {
if (sanitized[key]) {
sanitized[key] = '[REDACTED]';
}
}
return sanitized;
}
}
// Usage
await auditService.logSecurityEvent('LOGIN_SUCCESS', user.id, {
email: user.email,
ipAddress: req.ip
}, req);
await auditService.logSecurityEvent('PERMISSION_DENIED', user.id, {
resource: 'users',
action: 'delete',
targetId: targetUserId
}, req);
Security Headers
// Add security headers middleware
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Additional headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
next();
});
CORS Configuration
// Configure CORS securely
import cors from 'cors';
const allowedOrigins = process.env.CORS_ORIGIN?.split(',') || [];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Request-ID'],
maxAge: 86400 // 24 hours
}));
Security Testing
// Security test patterns
describe('Security Tests', () => {
it('should prevent SQL injection', async () => {
const maliciousInput = "'; DROP TABLE users; --";
const response = await request(app)
.get(`/api/v1/users?search=${encodeURIComponent(maliciousInput)}`)
.set('Authorization', `Bearer ${token}`);
expect(response.status).not.toBe(500);
// Should return 400 or empty results, not crash
});
it('should prevent XSS attacks', async () => {
const xssPayload = '<script>alert("XSS")</script>';
const response = await request(app)
.post('/api/v1/users')
.send({ email: xssPayload, password: 'test123' });
// Response should sanitize or reject
expect(response.body.data?.email).not.toContain('<script>');
});
it('should enforce authentication', async () => {
const response = await request(app)
.get('/api/v1/users');
expect(response.status).toBe(401);
});
it('should enforce authorization', async () => {
const userToken = await createUserToken({ roles: ['user'] });
const response = await request(app)
.delete('/api/v1/users/123')
.set('Authorization', `Bearer ${userToken}`);
expect(response.status).toBe(403);
});
it('should rate limit excessive requests', async () => {
const requests = Array(20).fill(null).map(() =>
request(app).get('/api/v1/users')
);
const responses = await Promise.all(requests);
const rateLimited = responses.filter(r => r.status === 429);
expect(rateLimited.length).toBeGreaterThan(0);
});
});
Security Checklist
Before deploying any service:
- All endpoints require authentication (except public)
- Authorization checks implemented (RBAC/ABAC)
- Input validation with Zod schemas
- Rate limiting configured
- Error messages sanitized (no info disclosure)
- PII encrypted at rest
- Passwords hashed with bcrypt (cost 12+)
- Tokens hashed before storing
- Secrets in environment variables (never hardcoded)
- HTTPS enforced (TLS 1.2+)
- CORS configured correctly
- Security headers set (helmet)
- Audit logging enabled
- SQL injection prevented (use Prisma)
- XSS prevention (input sanitization)
- File upload validation
- Security tests passing
- Dependencies scanned for vulnerabilities
- Secrets rotation plan in place
Common Security Anti-Patterns
// ❌ BAD: Hardcoded secrets
const SECRET = 'my-secret-key';
// ✅ GOOD: Environment variables
const SECRET = process.env.JWT_SECRET;
// ❌ BAD: Plain text passwords
await prisma.user.create({ data: { password: password } });
// ✅ GOOD: Hashed passwords
await prisma.user.create({
data: { passwordHash: await bcrypt.hash(password, 12) }
});
// ❌ BAD: Exposing user existence
if (!user) {
throw new Error('User not found'); // Reveals user doesn't exist
}
// ✅ GOOD: Generic error
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
throw new Error('Invalid credentials');
}
// ❌ BAD: No input validation
const email = req.body.email;
// ✅ GOOD: Validate with Zod
const { email } = CreateUserDto.parse(req.body);
// ❌ BAD: Stack traces in production
res.status(500).json({ error: error.stack });
// ✅ GOOD: Sanitized errors
res.status(500).json({
error: { code: 'INTERNAL_ERROR', message: 'An error occurred' }
});
Incident Response
// Security incident detection and response
export class SecurityIncidentService {
async detectAnomaly(userId: string, event: string, context: any) {
// Check for suspicious patterns
const recentEvents = await this.getRecentEvents(userId, '1h');
if (recentEvents.length > 10) {
await this.triggerAlert('SUSPICIOUS_ACTIVITY', {
userId,
eventCount: recentEvents.length,
timeWindow: '1h'
});
}
// Check for privilege escalation attempts
if (event === 'PERMISSION_DENIED' && context.requiredPermission) {
await this.logSecurityEvent('PRIVILEGE_ESCALATION_ATTEMPT', userId, context);
}
}
async triggerAlert(type: string, details: any) {
// Send to monitoring system
logger.error('Security alert', { type, details });
// TODO: Integrate with PagerDuty, Slack, etc.
}
}