File size: 4,358 Bytes
4554903
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# 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);
```