# Security Patterns for Multiverse Campus ## Environment Variable Safety ```typescript // NEVER do this: const secret = process.env.SECRET || ''; // Empty string bypass! // ALWAYS do this: function requireEnv(name: string): string { const value = process.env[name]; if (!value) { console.error(`FATAL: ${name} is not set. Refusing to start.`); process.exit(1); } return value; } const JWT_SECRET = requireEnv('JWT_SECRET'); const WEBHOOK_SECRET = requireEnv('SCHOOL_WEBHOOK_SECRET'); ``` ## Webhook Signature Verification ```typescript import crypto from 'crypto'; function verifyWebhookSignature(payload: Buffer, signature: string, secret: string): boolean { // NEVER skip verification — reject if secret is missing if (!secret) { throw new Error('Webhook secret not configured — rejecting all requests'); } const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(`sha256=${expected}`) ); } // Route handler router.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-signature'] as string; if (!verifyWebhookSignature(req.body, sig, WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process webhook... }); ``` ## JWT Best Practices ```typescript // Token creation — minimal claims, short expiry function createAccessToken(student: { id: string; email: string }): string { return jwt.sign( { sub: student.id, email: student.email }, JWT_SECRET, { expiresIn: '15m', algorithm: 'HS256' } ); } // Token verification — explicit algorithm, clock tolerance function verifyToken(token: string): JwtPayload { return jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'], clockTolerance: 30, // 30 second clock skew tolerance }) as JwtPayload; } // NEVER put mutable data (roles, permissions) in the access token // unless you accept the 15-min staleness window. // For real-time permission checks, hit the database. ``` ## Socket.IO Auth Pattern ```typescript io.use(async (socket, next) => { const token = socket.handshake.auth?.token; if (!token) { return next(new Error('Authentication required')); } try { const payload = verifyToken(token); socket.userId = payload.sub; socket.email = payload.email; next(); } catch (err) { next(new Error('Invalid or expired token')); } }); // Reconnection: exponential backoff on client const socket = io(SERVER_URL, { auth: { token: getAccessToken() }, reconnectionDelay: 1000, // Start at 1s reconnectionDelayMax: 30000, // Cap at 30s reconnectionAttempts: 10, // Give up after 10 }); socket.on('connect_error', async (err) => { if (err.message === 'Invalid or expired token') { // Only refresh on auth errors, not all errors const newToken = await refreshAccessToken(); socket.auth = { token: newToken }; socket.connect(); } // For other errors, let the built-in backoff handle it }); ``` ## Input Validation ```typescript import { z } from 'zod'; // Define schema const CreateResourceSchema = z.object({ name: z.string().min(1).max(255), type: z.enum(['quest', 'achievement', 'item']), value: z.number().int().min(0).max(10000), }); // Validate in route router.post('/resource', async (req, res) => { const parsed = CreateResourceSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ success: false, errors: parsed.error.flatten().fieldErrors, }); } // parsed.data is fully typed and validated const result = await service.create(parsed.data); res.json({ success: true, data: result }); }); ``` ## Rate Limiting ```typescript import rateLimit from 'express-rate-limit'; // Global rate limit const globalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window standardHeaders: true, legacyHeaders: false, }); // Strict limit for auth endpoints const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // Only 5 login attempts per 15 min message: { error: 'Too many attempts, please try again later' }, }); app.use('/api/', globalLimiter); app.use('/api/auth/login', authLimiter); ```