--- name: security description: 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 1. **Defense in Depth**: Multiple layers of security controls 2. **Least Privilege**: Grant minimum required permissions 3. **Fail Secure**: Default to deny access 4. **Separation of Duties**: Critical operations require multiple approvals 5. **Audit Everything**: Log all security-relevant events 6. **Encrypt Sensitive Data**: PII, tokens, credentials must be encrypted 7. **Validate All Inputs**: Never trust user input 8. **Principle of Least Exposure**: Minimize attack surface 9. **Secure by Default**: Security built-in, not bolted on 10. **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: ```mermaid 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
Authorization header or cookie alt Token not found Middleware->>Client: 401 Unauthorized
(AUTH_REQUIRED) else Token found Middleware->>JWTService: verifyAccessToken(token) alt Token invalid or expired JWTService->>Middleware: Verification failed Middleware->>Client: 401 Unauthorized
(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 ``` ```typescript // 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: ```mermaid flowchart TD Start([Request Received]) --> CheckAuth{User
Authenticated?} CheckAuth -->|No| Return401[Return 401
AUTH_REQUIRED] CheckAuth -->|Yes| CheckType{Authorization
Type?} CheckType -->|Role-Based| CheckRole{User has
Required Role?} CheckType -->|Permission-Based| CheckPermission{User has
Resource:Action
Permission?} CheckType -->|Ownership| CheckOwnership{Resource ID
matches User ID?} CheckRole -->|No| LogDenial[Log Permission Denied
with user roles] CheckPermission -->|No| LogDenial CheckOwnership -->|No| LogDenial LogDenial --> Return403[Return 403
FORBIDDEN] CheckRole -->|Yes| Allow[Allow Request
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 ``` ```typescript // 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 ```typescript // 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: ```mermaid 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
(16 bytes) EncryptionService->>Crypto: Create cipher
(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
(AES-256-GCM) EncryptionService->>Crypto: Set auth tag EncryptionService->>Crypto: Decrypt ciphertext Crypto->>EncryptionService: Plaintext EncryptionService->>Service: Return plaintext ``` ```typescript // 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: ```mermaid 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
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
(generic error) end ``` ```typescript // 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 { return bcrypt.hash(password, SALT_ROUNDS); } async verify(password: string, hash: string): Promise { 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 { // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // Log all security-relevant events export class AuditService { async logSecurityEvent( event: string, userId: string | null, details: Record, 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): Record { 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 = ''; 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('