| # 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); |
| ``` |
|
|