yinzerflow 0.5.6 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/modules/cookie-parser.md +982 -0
- package/index.d.ts +226 -0
- package/index.js +17 -17
- package/index.js.map +12 -9
- package/package.json +1 -1
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
# 📖 Overview
|
|
2
|
+
|
|
3
|
+
YinzerFlow provides a built-in cookie parser that handles parsing incoming cookies, setting outgoing cookies, and optionally signing/validating cookies using HMAC-SHA256. The cookie parser is <span style="color: #95a5a6">**disabled by default**</span> and must be explicitly enabled in the YinzerFlow constructor when needed.
|
|
4
|
+
|
|
5
|
+
<span style="color: #3498db">**💡 Tip:**</span> The cookie parser makes it easy to work with session management and user preferences.
|
|
6
|
+
|
|
7
|
+
**When to use:**
|
|
8
|
+
|
|
9
|
+
- 🍪 Session management (store session IDs)
|
|
10
|
+
- 👤 User authentication tokens (JWT refresh tokens)
|
|
11
|
+
- 🎨 User preferences (theme, language)
|
|
12
|
+
- 📊 Analytics and tracking
|
|
13
|
+
|
|
14
|
+
**Expected outcomes:**
|
|
15
|
+
|
|
16
|
+
- ✅ Automatic parsing of incoming `Cookie` headers into Maps
|
|
17
|
+
- ✅ Helper methods for setting cookies with attributes
|
|
18
|
+
- 🛡️ HMAC signature validation to detect tampering
|
|
19
|
+
- 📝 Type-safe cookie access on request and context objects
|
|
20
|
+
|
|
21
|
+
# ⚙️ Usage
|
|
22
|
+
|
|
23
|
+
## 🎛️ Settings
|
|
24
|
+
|
|
25
|
+
### enabled — @default <span style="color: #95a5a6">`false`</span>
|
|
26
|
+
|
|
27
|
+
Enable or disable the cookie parser. Must be explicitly enabled to use cookie parsing.
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
31
|
+
|
|
32
|
+
// Enable cookie parser
|
|
33
|
+
const app = new YinzerFlow({
|
|
34
|
+
port: 3000,
|
|
35
|
+
cookieParser: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
secret: process.env.COOKIE_SECRET
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Disabled by default (no configuration needed)
|
|
42
|
+
const app = new YinzerFlow({ port: 3000 });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### secret — @default <span style="color: #95a5a6">`undefined`</span>
|
|
46
|
+
|
|
47
|
+
Secret key for signing cookies using HMAC-SHA256. When provided, cookies can be signed to detect tampering.
|
|
48
|
+
|
|
49
|
+
<span style="color: #e74c3c">**⚠️ Warning:**</span> In production, always use a strong secret (at least 32 characters) stored in environment variables.
|
|
50
|
+
|
|
51
|
+
#### What Makes a Strong Secret?
|
|
52
|
+
|
|
53
|
+
A strong secret should be:
|
|
54
|
+
- **Random**: Generated using cryptographically secure random methods
|
|
55
|
+
- **Long**: Minimum 32 characters (256 bits of entropy)
|
|
56
|
+
- **Unique**: Different for each application/environment
|
|
57
|
+
- **Secret**: Never committed to version control
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
const app = new YinzerFlow({
|
|
61
|
+
port: 3000,
|
|
62
|
+
cookieParser: {
|
|
63
|
+
secret: process.env.COOKIE_SECRET
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
#### How to Generate a Strong Secret
|
|
69
|
+
|
|
70
|
+
**Option 1: Using Node.js crypto (recommended)**
|
|
71
|
+
```bash
|
|
72
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
73
|
+
# Output: 4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
|
|
74
|
+
|
|
75
|
+
# Use the output in your .env file:
|
|
76
|
+
# COOKIE_SECRET=4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Option 2: Using OpenSSL**
|
|
80
|
+
```bash
|
|
81
|
+
openssl rand -hex 32
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Option 3: Online generators**
|
|
85
|
+
- Use trusted services like 1Password Secret Key Generator (for reference, not production)
|
|
86
|
+
- Generate locally and store securely
|
|
87
|
+
|
|
88
|
+
#### ❌ What NOT to Use
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// ❌ Weak - predictable
|
|
92
|
+
secret: 'my-secret-key'
|
|
93
|
+
|
|
94
|
+
// ❌ Weak - too short
|
|
95
|
+
secret: '12345678'
|
|
96
|
+
|
|
97
|
+
// ❌ Weak - based on company name
|
|
98
|
+
secret: 'acmecorp2024'
|
|
99
|
+
|
|
100
|
+
// ✅ Strong - random and long
|
|
101
|
+
secret: '4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### signed — @default <span style="color: #95a5a6">`undefined`</span> (sign all)
|
|
105
|
+
|
|
106
|
+
Array of cookie names to sign. If undefined or empty, all cookies are signed when a secret is provided.
|
|
107
|
+
|
|
108
|
+
<span style="color: #3498db">**💡 Tip:**</span> Only sign cookies that need integrity protection (session IDs, user tokens).
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// Sign all cookies (default)
|
|
112
|
+
const app = new YinzerFlow({
|
|
113
|
+
port: 3000,
|
|
114
|
+
cookieParser: {
|
|
115
|
+
secret: process.env.COOKIE_SECRET
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Sign specific cookies only
|
|
120
|
+
const app = new YinzerFlow({
|
|
121
|
+
port: 3000,
|
|
122
|
+
cookieParser: {
|
|
123
|
+
secret: process.env.COOKIE_SECRET,
|
|
124
|
+
signed: ['sessionId', 'userId']
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### defaults — @default <span style="color: #95a5a6">`undefined`</span>
|
|
130
|
+
|
|
131
|
+
Default cookie options applied to all cookies set via `ctx.cookies.set()`. Can be overridden per-cookie.
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const app = new YinzerFlow({
|
|
135
|
+
port: 3000,
|
|
136
|
+
cookieParser: {
|
|
137
|
+
secret: process.env.COOKIE_SECRET,
|
|
138
|
+
defaults: {
|
|
139
|
+
httpOnly: true, // Prevent JavaScript access
|
|
140
|
+
secure: true, // HTTPS only
|
|
141
|
+
sameSite: 'strict', // CSRF protection
|
|
142
|
+
maxAge: '1h' // 1 hour using TimeString (or 3600 as seconds)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
<aside>
|
|
149
|
+
|
|
150
|
+
Cookie Options:
|
|
151
|
+
|
|
152
|
+
- **httpOnly**: Prevent JavaScript access (recommended for auth cookies)
|
|
153
|
+
- **secure**: Only send over HTTPS (required in production)
|
|
154
|
+
- **sameSite**: CSRF protection - 'strict' (best), 'lax', or 'none' (requires secure)
|
|
155
|
+
- **maxAge**: Cookie lifetime - accepts `'30s'`, `'15m'`, `'2h'`, `'1d'` or seconds as number
|
|
156
|
+
- **expires**: Expiration date (alternative to maxAge)
|
|
157
|
+
- **domain**: Cookie domain
|
|
158
|
+
- **path**: Cookie path (must start with '/')
|
|
159
|
+
|
|
160
|
+
</aside>
|
|
161
|
+
|
|
162
|
+
# 🔧 Basic Usage
|
|
163
|
+
|
|
164
|
+
## Parsing Cookies
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
app.get('/api/user', async (ctx) => {
|
|
168
|
+
// Access unsigned cookies
|
|
169
|
+
const theme = ctx.request.cookies.get('theme');
|
|
170
|
+
|
|
171
|
+
// Access signed cookies (validated)
|
|
172
|
+
const sessionId = ctx.request.signedCookies.get('sessionId');
|
|
173
|
+
|
|
174
|
+
return { theme, sessionId };
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Setting Cookies
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
app.post('/api/login', async (ctx) => {
|
|
182
|
+
const user = await validateUser(ctx.request.body);
|
|
183
|
+
|
|
184
|
+
// Set session cookie
|
|
185
|
+
ctx.cookies.set('sessionId', user.sessionId, {
|
|
186
|
+
httpOnly: true,
|
|
187
|
+
secure: true,
|
|
188
|
+
sameSite: 'strict',
|
|
189
|
+
maxAge: '1h' // or 3600 for 1 hour in seconds
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { success: true };
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Setting Multiple Cookies
|
|
197
|
+
|
|
198
|
+
You can set multiple cookies in a single handler. Each call to `ctx.cookies.set()` will add a separate `Set-Cookie` header to the response:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
app.post('/api/set-preferences', async (ctx) => {
|
|
202
|
+
// Set multiple cookies - each creates its own Set-Cookie header
|
|
203
|
+
ctx.cookies.set('theme', 'dark', {
|
|
204
|
+
httpOnly: false, // Allow JavaScript access
|
|
205
|
+
maxAge: 31536000 // 1 year
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
ctx.cookies.set('language', 'en', {
|
|
209
|
+
httpOnly: false,
|
|
210
|
+
maxAge: 31536000
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
ctx.cookies.set('sessionId', user.sessionId, {
|
|
214
|
+
httpOnly: true, // Secure session cookie
|
|
215
|
+
secure: true,
|
|
216
|
+
sameSite: 'strict',
|
|
217
|
+
maxAge: 3600
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return { success: true };
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Signed Cookies
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Sign and set a cookie
|
|
228
|
+
const signedValue = ctx.cookies.sign('sessionId', 'abc123');
|
|
229
|
+
ctx.cookies.set('sessionId', signedValue, {
|
|
230
|
+
httpOnly: true,
|
|
231
|
+
secure: true
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Validate signed cookies
|
|
235
|
+
const sessionId = ctx.request.signedCookies.get('sessionId');
|
|
236
|
+
if (!sessionId) {
|
|
237
|
+
throw new Error('Session cookie missing or tampered');
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
# 💻 Examples
|
|
242
|
+
|
|
243
|
+
## JWT Refresh Token Flow
|
|
244
|
+
|
|
245
|
+
**Use Case:** Secure authentication with JWT access tokens and refresh tokens
|
|
246
|
+
|
|
247
|
+
**Description:** Production-ready JWT authentication where short-lived access tokens are used for API calls, and long-lived refresh tokens are stored in httpOnly cookies.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
251
|
+
import jwt from 'jsonwebtoken';
|
|
252
|
+
|
|
253
|
+
const app = new YinzerFlow({
|
|
254
|
+
port: 3000,
|
|
255
|
+
cookieParser: {
|
|
256
|
+
enabled: true,
|
|
257
|
+
secret: process.env.COOKIE_SECRET,
|
|
258
|
+
defaults: {
|
|
259
|
+
httpOnly: true,
|
|
260
|
+
secure: true,
|
|
261
|
+
sameSite: 'strict'
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Constants for token lifetime
|
|
267
|
+
const ACCESS_TOKEN_EXPIRY = 15 * 60; // 15 minutes
|
|
268
|
+
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days (in seconds)
|
|
269
|
+
|
|
270
|
+
// Login endpoint
|
|
271
|
+
app.post('/api/auth/login', async (ctx) => {
|
|
272
|
+
const { username, password } = ctx.request.body;
|
|
273
|
+
|
|
274
|
+
// Authenticate user
|
|
275
|
+
const user = await validateUser(username, password);
|
|
276
|
+
if (!user) {
|
|
277
|
+
ctx.response.setStatusCode(401);
|
|
278
|
+
return { error: 'Invalid credentials' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Generate short-lived access token
|
|
282
|
+
const accessToken = jwt.sign(
|
|
283
|
+
{ userId: user.id, type: 'access' },
|
|
284
|
+
process.env.JWT_SECRET,
|
|
285
|
+
{ expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// Generate long-lived refresh token
|
|
289
|
+
const refreshToken = jwt.sign(
|
|
290
|
+
{ userId: user.id, type: 'refresh' },
|
|
291
|
+
process.env.REFRESH_SECRET,
|
|
292
|
+
{ expiresIn: `${REFRESH_TOKEN_EXPIRY}s` }
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Store refresh token in cookie with matching maxAge
|
|
296
|
+
ctx.cookies.set('refreshToken', refreshToken, {
|
|
297
|
+
maxAge: REFRESH_TOKEN_EXPIRY // Same as JWT expiration!
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Return access token in response body
|
|
301
|
+
return { accessToken };
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Token refresh endpoint
|
|
305
|
+
app.post('/api/auth/refresh', async (ctx) => {
|
|
306
|
+
const refreshToken = ctx.request.signedCookies.get('refreshToken');
|
|
307
|
+
|
|
308
|
+
if (!refreshToken) {
|
|
309
|
+
ctx.response.setStatusCode(401);
|
|
310
|
+
return { error: 'Refresh token missing' };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// Verify refresh token
|
|
315
|
+
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
|
|
316
|
+
|
|
317
|
+
// Generate new access token
|
|
318
|
+
const accessToken = jwt.sign(
|
|
319
|
+
{ userId: decoded.userId, type: 'access' },
|
|
320
|
+
process.env.JWT_SECRET,
|
|
321
|
+
{ expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
return { accessToken };
|
|
325
|
+
} catch (error) {
|
|
326
|
+
ctx.response.setStatusCode(401);
|
|
327
|
+
return { error: 'Invalid refresh token' };
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Protected endpoint using access token
|
|
332
|
+
app.get('/api/user/profile', async (ctx) => {
|
|
333
|
+
const authHeader = ctx.request.headers.authorization;
|
|
334
|
+
const accessToken = authHeader?.replace('Bearer ', '');
|
|
335
|
+
|
|
336
|
+
if (!accessToken) {
|
|
337
|
+
ctx.response.setStatusCode(401);
|
|
338
|
+
return { error: 'Access token required' };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
|
|
343
|
+
const user = await getUserById(decoded.userId);
|
|
344
|
+
return { user };
|
|
345
|
+
} catch (error) {
|
|
346
|
+
ctx.response.setStatusCode(401);
|
|
347
|
+
return { error: 'Invalid access token' };
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await app.listen();
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Session Management
|
|
355
|
+
|
|
356
|
+
**Use Case:** Secure session management with signed cookies
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
360
|
+
|
|
361
|
+
const app = new YinzerFlow({
|
|
362
|
+
port: 3000,
|
|
363
|
+
cookieParser: {
|
|
364
|
+
enabled: true,
|
|
365
|
+
secret: process.env.COOKIE_SECRET,
|
|
366
|
+
defaults: {
|
|
367
|
+
httpOnly: true,
|
|
368
|
+
secure: true,
|
|
369
|
+
sameSite: 'strict',
|
|
370
|
+
maxAge: 3600 // 1 hour
|
|
371
|
+
},
|
|
372
|
+
signed: ['sessionId', 'userId']
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
app.post('/api/auth/login', async (ctx) => {
|
|
377
|
+
const user = await authenticate(ctx.request.body);
|
|
378
|
+
|
|
379
|
+
const sessionId = generateSessionId();
|
|
380
|
+
await saveSession(sessionId, user.id);
|
|
381
|
+
|
|
382
|
+
const signedSessionId = ctx.cookies.sign('sessionId', sessionId);
|
|
383
|
+
ctx.cookies.set('sessionId', signedSessionId);
|
|
384
|
+
|
|
385
|
+
return { success: true };
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
app.get('/api/protected', async (ctx) => {
|
|
389
|
+
const sessionId = ctx.request.signedCookies.get('sessionId');
|
|
390
|
+
if (!sessionId) {
|
|
391
|
+
ctx.response.setStatusCode(401);
|
|
392
|
+
return { error: 'Not authenticated' };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const userId = await getUserIdFromSession(sessionId);
|
|
396
|
+
const user = await getUserById(userId);
|
|
397
|
+
|
|
398
|
+
return { user };
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await app.listen();
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Development Setup
|
|
405
|
+
|
|
406
|
+
**Use Case:** Development server with relaxed cookie settings
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
410
|
+
|
|
411
|
+
const app = new YinzerFlow({
|
|
412
|
+
port: 3000,
|
|
413
|
+
networkLogs: true,
|
|
414
|
+
cookieParser: {
|
|
415
|
+
enabled: true,
|
|
416
|
+
defaults: {
|
|
417
|
+
httpOnly: false, // Allow JavaScript access
|
|
418
|
+
secure: false, // Allow HTTP
|
|
419
|
+
sameSite: 'lax'
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
app.get('/api/test', async (ctx) => {
|
|
425
|
+
ctx.cookies.set('theme', 'dark');
|
|
426
|
+
return { theme: ctx.request.cookies.get('theme') };
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
await app.listen();
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
# ✨ Best Practices
|
|
433
|
+
|
|
434
|
+
- 🛡️ **Always use `secret`**: Sign authentication cookies to detect tampering
|
|
435
|
+
- 🔒 **Use `httpOnly: true`**: Prevent JavaScript access to sensitive cookies
|
|
436
|
+
- 🔐 **Use `secure: true`**: Only send cookies over HTTPS in production
|
|
437
|
+
- 🎯 **Use `sameSite: 'strict'`**: Best CSRF protection
|
|
438
|
+
- ⏱️ **Match `maxAge` to JWT expiration**: For refresh tokens, set cookie `maxAge` to match JWT `expiresIn`
|
|
439
|
+
- 🔑 **Store secrets in environment variables**: Never commit secrets
|
|
440
|
+
- 📊 **Use signed cookies for sensitive data**: Session IDs, user tokens
|
|
441
|
+
- 💡 **Use unsigned cookies for preferences**: Theme, language
|
|
442
|
+
|
|
443
|
+
## 🚀 Performance Notes
|
|
444
|
+
|
|
445
|
+
- ⚡ **Parsing overhead**: Minimal - cookies are parsed once per request
|
|
446
|
+
- 💾 **Memory usage**: Maps are garbage collected after request
|
|
447
|
+
- 🔐 **HMAC performance**: Fast - HMAC-SHA256 is efficient (~1ms per cookie)
|
|
448
|
+
- 📊 **Signature size**: ~43 characters per signed cookie (base64url)
|
|
449
|
+
|
|
450
|
+
## 🔒 Security Notes
|
|
451
|
+
|
|
452
|
+
YinzerFlow implements several security measures to protect against common cookie vulnerabilities:
|
|
453
|
+
|
|
454
|
+
### 🛡️ HMAC Signature Validation
|
|
455
|
+
- **Problem**: Cookies can be tampered with by clients
|
|
456
|
+
- **YinzerFlow Solution**: HMAC-SHA256 signatures detect tampering
|
|
457
|
+
|
|
458
|
+
### 🛡️ XSS Protection
|
|
459
|
+
- **Problem**: JavaScript can access cookies via `document.cookie`
|
|
460
|
+
- **YinzerFlow Solution**: `httpOnly: true` prevents JavaScript access
|
|
461
|
+
|
|
462
|
+
### 🛡️ Man-in-the-Middle Attacks
|
|
463
|
+
- **Problem**: Cookies sent over HTTP can be intercepted
|
|
464
|
+
- **YinzerFlow Solution**: `secure: true` ensures HTTPS-only transmission
|
|
465
|
+
|
|
466
|
+
### 🛡️ CSRF Protection
|
|
467
|
+
- **Problem**: Cross-site requests can include cookies
|
|
468
|
+
- **YinzerFlow Solution**: `sameSite: 'strict'` blocks cross-site cookie sends
|
|
469
|
+
|
|
470
|
+
## 🔧 Troubleshooting
|
|
471
|
+
|
|
472
|
+
### Cookies are not being set
|
|
473
|
+
**Symptom**: Cookies don't appear in browser
|
|
474
|
+
|
|
475
|
+
**Solution:**
|
|
476
|
+
- Enable cookie parser in configuration (`enabled: true`)
|
|
477
|
+
- Check `secure: true` only for HTTPS connections
|
|
478
|
+
- Verify domain/path match
|
|
479
|
+
|
|
480
|
+
### Signed cookies return `false` when unsigned
|
|
481
|
+
**Symptom**: `ctx.request.signedCookies` is empty or returns false
|
|
482
|
+
|
|
483
|
+
**Solution:**
|
|
484
|
+
- Verify secret is correct
|
|
485
|
+
- Ensure cookie was properly signed using `ctx.cookies.sign()`
|
|
486
|
+
- Check if cookie was tampered with# 📖 Overview
|
|
487
|
+
|
|
488
|
+
YinzerFlow provides a built-in cookie parser that handles parsing incoming cookies, setting outgoing cookies, and optionally signing/validating cookies using HMAC-SHA256. The cookie parser is <span style="color: #95a5a6">**disabled by default**</span> and must be explicitly enabled in the YinzerFlow constructor when needed.
|
|
489
|
+
|
|
490
|
+
<span style="color: #3498db">**💡 Tip:**</span> The cookie parser makes it easy to work with session management and user preferences.
|
|
491
|
+
|
|
492
|
+
**When to use:**
|
|
493
|
+
|
|
494
|
+
- 🍪 Session management (store session IDs)
|
|
495
|
+
- 👤 User authentication tokens (JWT refresh tokens)
|
|
496
|
+
- 🎨 User preferences (theme, language)
|
|
497
|
+
- 📊 Analytics and tracking
|
|
498
|
+
|
|
499
|
+
**Expected outcomes:**
|
|
500
|
+
|
|
501
|
+
- ✅ Automatic parsing of incoming `Cookie` headers into Maps
|
|
502
|
+
- ✅ Helper methods for setting cookies with attributes
|
|
503
|
+
- 🛡️ HMAC signature validation to detect tampering
|
|
504
|
+
- 📝 Type-safe cookie access on request and context objects
|
|
505
|
+
|
|
506
|
+
# ⚙️ Usage
|
|
507
|
+
|
|
508
|
+
## 🎛️ Settings
|
|
509
|
+
|
|
510
|
+
### enabled — @default <span style="color: #95a5a6">`false`</span>
|
|
511
|
+
|
|
512
|
+
Enable or disable the cookie parser. Must be explicitly enabled to use cookie parsing.
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
516
|
+
|
|
517
|
+
// Enable cookie parser
|
|
518
|
+
const app = new YinzerFlow({
|
|
519
|
+
port: 3000,
|
|
520
|
+
cookieParser: {
|
|
521
|
+
enabled: true,
|
|
522
|
+
secret: process.env.COOKIE_SECRET
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Disabled by default (no configuration needed)
|
|
527
|
+
const app = new YinzerFlow({ port: 3000 });
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### secret — @default <span style="color: #95a5a6">`undefined`</span>
|
|
531
|
+
|
|
532
|
+
Secret key for signing cookies using HMAC-SHA256. When provided, cookies can be signed to detect tampering.
|
|
533
|
+
|
|
534
|
+
<span style="color: #e74c3c">**⚠️ Warning:**</span> In production, always use a strong secret (at least 32 characters) stored in environment variables.
|
|
535
|
+
|
|
536
|
+
#### What Makes a Strong Secret?
|
|
537
|
+
|
|
538
|
+
A strong secret should be:
|
|
539
|
+
- **Random**: Generated using cryptographically secure random methods
|
|
540
|
+
- **Long**: Minimum 32 characters (256 bits of entropy)
|
|
541
|
+
- **Unique**: Different for each application/environment
|
|
542
|
+
- **Secret**: Never committed to version control
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
const app = new YinzerFlow({
|
|
546
|
+
port: 3000,
|
|
547
|
+
cookieParser: {
|
|
548
|
+
secret: process.env.COOKIE_SECRET
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
#### How to Generate a Strong Secret
|
|
554
|
+
|
|
555
|
+
**Option 1: Using Node.js crypto (recommended)**
|
|
556
|
+
```bash
|
|
557
|
+
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
558
|
+
# Output: 4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
|
|
559
|
+
|
|
560
|
+
# Use the output in your .env file:
|
|
561
|
+
# COOKIE_SECRET=4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
**Option 2: Using OpenSSL**
|
|
565
|
+
```bash
|
|
566
|
+
openssl rand -hex 32
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**Option 3: Online generators**
|
|
570
|
+
- Use trusted services like 1Password Secret Key Generator (for reference, not production)
|
|
571
|
+
- Generate locally and store securely
|
|
572
|
+
|
|
573
|
+
#### ❌ What NOT to Use
|
|
574
|
+
|
|
575
|
+
```typescript
|
|
576
|
+
// ❌ Weak - predictable
|
|
577
|
+
secret: 'my-secret-key'
|
|
578
|
+
|
|
579
|
+
// ❌ Weak - too short
|
|
580
|
+
secret: '12345678'
|
|
581
|
+
|
|
582
|
+
// ❌ Weak - based on company name
|
|
583
|
+
secret: 'acmecorp2024'
|
|
584
|
+
|
|
585
|
+
// ✅ Strong - random and long
|
|
586
|
+
secret: '4a8f2c9d1e6b5a3c7f8e2d9b1a4c6e8f2d7b9a3c5e8f1d6b4a2'
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
### signed — @default <span style="color: #95a5a6">`undefined`</span> (sign all)
|
|
590
|
+
|
|
591
|
+
Array of cookie names to sign. If undefined or empty, all cookies are signed when a secret is provided.
|
|
592
|
+
|
|
593
|
+
<span style="color: #3498db">**💡 Tip:**</span> Only sign cookies that need integrity protection (session IDs, user tokens).
|
|
594
|
+
|
|
595
|
+
```typescript
|
|
596
|
+
// Sign all cookies (default)
|
|
597
|
+
const app = new YinzerFlow({
|
|
598
|
+
port: 3000,
|
|
599
|
+
cookieParser: {
|
|
600
|
+
secret: process.env.COOKIE_SECRET
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Sign specific cookies only
|
|
605
|
+
const app = new YinzerFlow({
|
|
606
|
+
port: 3000,
|
|
607
|
+
cookieParser: {
|
|
608
|
+
secret: process.env.COOKIE_SECRET,
|
|
609
|
+
signed: ['sessionId', 'userId']
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
### defaults — @default <span style="color: #95a5a6">`undefined`</span>
|
|
615
|
+
|
|
616
|
+
Default cookie options applied to all cookies set via `ctx.cookies.set()`. Can be overridden per-cookie.
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
const app = new YinzerFlow({
|
|
620
|
+
port: 3000,
|
|
621
|
+
cookieParser: {
|
|
622
|
+
secret: process.env.COOKIE_SECRET,
|
|
623
|
+
defaults: {
|
|
624
|
+
httpOnly: true, // Prevent JavaScript access
|
|
625
|
+
secure: true, // HTTPS only
|
|
626
|
+
sameSite: 'strict', // CSRF protection
|
|
627
|
+
maxAge: '1h' // 1 hour using TimeString (or 3600 as seconds)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
<aside>
|
|
634
|
+
|
|
635
|
+
Cookie Options:
|
|
636
|
+
|
|
637
|
+
- **httpOnly**: Prevent JavaScript access (recommended for auth cookies)
|
|
638
|
+
- **secure**: Only send over HTTPS (required in production)
|
|
639
|
+
- **sameSite**: CSRF protection - 'strict' (best), 'lax', or 'none' (requires secure)
|
|
640
|
+
- **maxAge**: Cookie lifetime - accepts `'30s'`, `'15m'`, `'2h'`, `'1d'` or seconds as number
|
|
641
|
+
- **expires**: Expiration date (alternative to maxAge)
|
|
642
|
+
- **domain**: Cookie domain
|
|
643
|
+
- **path**: Cookie path (must start with '/')
|
|
644
|
+
|
|
645
|
+
</aside>
|
|
646
|
+
|
|
647
|
+
# 🔧 Basic Usage
|
|
648
|
+
|
|
649
|
+
## Parsing Cookies
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
app.get('/api/user', async (ctx) => {
|
|
653
|
+
// Access unsigned cookies
|
|
654
|
+
const theme = ctx.request.cookies.get('theme');
|
|
655
|
+
|
|
656
|
+
// Access signed cookies (validated)
|
|
657
|
+
const sessionId = ctx.request.signedCookies.get('sessionId');
|
|
658
|
+
|
|
659
|
+
return { theme, sessionId };
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
## Setting Cookies
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
app.post('/api/login', async (ctx) => {
|
|
667
|
+
const user = await validateUser(ctx.request.body);
|
|
668
|
+
|
|
669
|
+
// Set session cookie
|
|
670
|
+
ctx.cookies.set('sessionId', user.sessionId, {
|
|
671
|
+
httpOnly: true,
|
|
672
|
+
secure: true,
|
|
673
|
+
sameSite: 'strict',
|
|
674
|
+
maxAge: '1h' // or 3600 for 1 hour in seconds
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
return { success: true };
|
|
678
|
+
});
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Setting Multiple Cookies
|
|
682
|
+
|
|
683
|
+
You can set multiple cookies in a single handler. Each call to `ctx.cookies.set()` will add a separate `Set-Cookie` header to the response:
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
app.post('/api/set-preferences', async (ctx) => {
|
|
687
|
+
// Set multiple cookies - each creates its own Set-Cookie header
|
|
688
|
+
ctx.cookies.set('theme', 'dark', {
|
|
689
|
+
httpOnly: false, // Allow JavaScript access
|
|
690
|
+
maxAge: 31536000 // 1 year
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
ctx.cookies.set('language', 'en', {
|
|
694
|
+
httpOnly: false,
|
|
695
|
+
maxAge: 31536000
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
ctx.cookies.set('sessionId', user.sessionId, {
|
|
699
|
+
httpOnly: true, // Secure session cookie
|
|
700
|
+
secure: true,
|
|
701
|
+
sameSite: 'strict',
|
|
702
|
+
maxAge: 3600
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
return { success: true };
|
|
706
|
+
});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
## Signed Cookies
|
|
710
|
+
|
|
711
|
+
```typescript
|
|
712
|
+
// Sign and set a cookie
|
|
713
|
+
const signedValue = ctx.cookies.sign('sessionId', 'abc123');
|
|
714
|
+
ctx.cookies.set('sessionId', signedValue, {
|
|
715
|
+
httpOnly: true,
|
|
716
|
+
secure: true
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
// Validate signed cookies
|
|
720
|
+
const sessionId = ctx.request.signedCookies.get('sessionId');
|
|
721
|
+
if (!sessionId) {
|
|
722
|
+
throw new Error('Session cookie missing or tampered');
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
# 💻 Examples
|
|
727
|
+
|
|
728
|
+
## JWT Refresh Token Flow
|
|
729
|
+
|
|
730
|
+
**Use Case:** Secure authentication with JWT access tokens and refresh tokens
|
|
731
|
+
|
|
732
|
+
**Description:** Production-ready JWT authentication where short-lived access tokens are used for API calls, and long-lived refresh tokens are stored in httpOnly cookies.
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
736
|
+
import jwt from 'jsonwebtoken';
|
|
737
|
+
|
|
738
|
+
const app = new YinzerFlow({
|
|
739
|
+
port: 3000,
|
|
740
|
+
cookieParser: {
|
|
741
|
+
enabled: true,
|
|
742
|
+
secret: process.env.COOKIE_SECRET,
|
|
743
|
+
defaults: {
|
|
744
|
+
httpOnly: true,
|
|
745
|
+
secure: true,
|
|
746
|
+
sameSite: 'strict'
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Constants for token lifetime
|
|
752
|
+
const ACCESS_TOKEN_EXPIRY = 15 * 60; // 15 minutes
|
|
753
|
+
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60; // 7 days (in seconds)
|
|
754
|
+
|
|
755
|
+
// Login endpoint
|
|
756
|
+
app.post('/api/auth/login', async (ctx) => {
|
|
757
|
+
const { username, password } = ctx.request.body;
|
|
758
|
+
|
|
759
|
+
// Authenticate user
|
|
760
|
+
const user = await validateUser(username, password);
|
|
761
|
+
if (!user) {
|
|
762
|
+
ctx.response.setStatusCode(401);
|
|
763
|
+
return { error: 'Invalid credentials' };
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Generate short-lived access token
|
|
767
|
+
const accessToken = jwt.sign(
|
|
768
|
+
{ userId: user.id, type: 'access' },
|
|
769
|
+
process.env.JWT_SECRET,
|
|
770
|
+
{ expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
// Generate long-lived refresh token
|
|
774
|
+
const refreshToken = jwt.sign(
|
|
775
|
+
{ userId: user.id, type: 'refresh' },
|
|
776
|
+
process.env.REFRESH_SECRET,
|
|
777
|
+
{ expiresIn: `${REFRESH_TOKEN_EXPIRY}s` }
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
// Store refresh token in cookie with matching maxAge
|
|
781
|
+
ctx.cookies.set('refreshToken', refreshToken, {
|
|
782
|
+
maxAge: REFRESH_TOKEN_EXPIRY // Same as JWT expiration!
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// Return access token in response body
|
|
786
|
+
return { accessToken };
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Token refresh endpoint
|
|
790
|
+
app.post('/api/auth/refresh', async (ctx) => {
|
|
791
|
+
const refreshToken = ctx.request.signedCookies.get('refreshToken');
|
|
792
|
+
|
|
793
|
+
if (!refreshToken) {
|
|
794
|
+
ctx.response.setStatusCode(401);
|
|
795
|
+
return { error: 'Refresh token missing' };
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
try {
|
|
799
|
+
// Verify refresh token
|
|
800
|
+
const decoded = jwt.verify(refreshToken, process.env.REFRESH_SECRET);
|
|
801
|
+
|
|
802
|
+
// Generate new access token
|
|
803
|
+
const accessToken = jwt.sign(
|
|
804
|
+
{ userId: decoded.userId, type: 'access' },
|
|
805
|
+
process.env.JWT_SECRET,
|
|
806
|
+
{ expiresIn: `${ACCESS_TOKEN_EXPIRY}s` }
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
return { accessToken };
|
|
810
|
+
} catch (error) {
|
|
811
|
+
ctx.response.setStatusCode(401);
|
|
812
|
+
return { error: 'Invalid refresh token' };
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// Protected endpoint using access token
|
|
817
|
+
app.get('/api/user/profile', async (ctx) => {
|
|
818
|
+
const authHeader = ctx.request.headers.authorization;
|
|
819
|
+
const accessToken = authHeader?.replace('Bearer ', '');
|
|
820
|
+
|
|
821
|
+
if (!accessToken) {
|
|
822
|
+
ctx.response.setStatusCode(401);
|
|
823
|
+
return { error: 'Access token required' };
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
|
|
828
|
+
const user = await getUserById(decoded.userId);
|
|
829
|
+
return { user };
|
|
830
|
+
} catch (error) {
|
|
831
|
+
ctx.response.setStatusCode(401);
|
|
832
|
+
return { error: 'Invalid access token' };
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
await app.listen();
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
## Session Management
|
|
840
|
+
|
|
841
|
+
**Use Case:** Secure session management with signed cookies
|
|
842
|
+
|
|
843
|
+
```typescript
|
|
844
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
845
|
+
|
|
846
|
+
const app = new YinzerFlow({
|
|
847
|
+
port: 3000,
|
|
848
|
+
cookieParser: {
|
|
849
|
+
enabled: true,
|
|
850
|
+
secret: process.env.COOKIE_SECRET,
|
|
851
|
+
defaults: {
|
|
852
|
+
httpOnly: true,
|
|
853
|
+
secure: true,
|
|
854
|
+
sameSite: 'strict',
|
|
855
|
+
maxAge: 3600 // 1 hour
|
|
856
|
+
},
|
|
857
|
+
signed: ['sessionId', 'userId']
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
app.post('/api/auth/login', async (ctx) => {
|
|
862
|
+
const user = await authenticate(ctx.request.body);
|
|
863
|
+
|
|
864
|
+
const sessionId = generateSessionId();
|
|
865
|
+
await saveSession(sessionId, user.id);
|
|
866
|
+
|
|
867
|
+
const signedSessionId = ctx.cookies.sign('sessionId', sessionId);
|
|
868
|
+
ctx.cookies.set('sessionId', signedSessionId);
|
|
869
|
+
|
|
870
|
+
return { success: true };
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
app.get('/api/protected', async (ctx) => {
|
|
874
|
+
const sessionId = ctx.request.signedCookies.get('sessionId');
|
|
875
|
+
if (!sessionId) {
|
|
876
|
+
ctx.response.setStatusCode(401);
|
|
877
|
+
return { error: 'Not authenticated' };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const userId = await getUserIdFromSession(sessionId);
|
|
881
|
+
const user = await getUserById(userId);
|
|
882
|
+
|
|
883
|
+
return { user };
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
await app.listen();
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
## Development Setup
|
|
890
|
+
|
|
891
|
+
**Use Case:** Development server with relaxed cookie settings
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
import { YinzerFlow } from 'yinzerflow';
|
|
895
|
+
|
|
896
|
+
const app = new YinzerFlow({
|
|
897
|
+
port: 3000,
|
|
898
|
+
networkLogs: true,
|
|
899
|
+
cookieParser: {
|
|
900
|
+
enabled: true,
|
|
901
|
+
defaults: {
|
|
902
|
+
httpOnly: false, // Allow JavaScript access
|
|
903
|
+
secure: false, // Allow HTTP
|
|
904
|
+
sameSite: 'lax'
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
app.get('/api/test', async (ctx) => {
|
|
910
|
+
ctx.cookies.set('theme', 'dark');
|
|
911
|
+
return { theme: ctx.request.cookies.get('theme') };
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
await app.listen();
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
# ✨ Best Practices
|
|
918
|
+
|
|
919
|
+
- 🛡️ **Always use `secret`**: Sign authentication cookies to detect tampering
|
|
920
|
+
- 🔒 **Use `httpOnly: true`**: Prevent JavaScript access to sensitive cookies
|
|
921
|
+
- 🔐 **Use `secure: true`**: Only send cookies over HTTPS in production
|
|
922
|
+
- 🎯 **Use `sameSite: 'strict'`**: Best CSRF protection
|
|
923
|
+
- ⏱️ **Match `maxAge` to JWT expiration**: For refresh tokens, set cookie `maxAge` to match JWT `expiresIn`
|
|
924
|
+
- 🔑 **Store secrets in environment variables**: Never commit secrets
|
|
925
|
+
- 📊 **Use signed cookies for sensitive data**: Session IDs, user tokens
|
|
926
|
+
- 💡 **Use unsigned cookies for preferences**: Theme, language
|
|
927
|
+
|
|
928
|
+
## 🚀 Performance Notes
|
|
929
|
+
|
|
930
|
+
- ⚡ **Parsing overhead**: Minimal - cookies are parsed once per request
|
|
931
|
+
- 💾 **Memory usage**: Maps are garbage collected after request
|
|
932
|
+
- 🔐 **HMAC performance**: Fast - HMAC-SHA256 is efficient (~1ms per cookie)
|
|
933
|
+
- 📊 **Signature size**: ~43 characters per signed cookie (base64url)
|
|
934
|
+
|
|
935
|
+
## 🔒 Security Notes
|
|
936
|
+
|
|
937
|
+
YinzerFlow implements several security measures to protect against common cookie vulnerabilities:
|
|
938
|
+
|
|
939
|
+
### 🛡️ HMAC Signature Validation
|
|
940
|
+
- **Problem**: Cookies can be tampered with by clients
|
|
941
|
+
- **YinzerFlow Solution**: HMAC-SHA256 signatures detect tampering
|
|
942
|
+
|
|
943
|
+
### 🛡️ XSS Protection
|
|
944
|
+
- **Problem**: JavaScript can access cookies via `document.cookie`
|
|
945
|
+
- **YinzerFlow Solution**: `httpOnly: true` prevents JavaScript access
|
|
946
|
+
|
|
947
|
+
### 🛡️ Man-in-the-Middle Attacks
|
|
948
|
+
- **Problem**: Cookies sent over HTTP can be intercepted
|
|
949
|
+
- **YinzerFlow Solution**: `secure: true` ensures HTTPS-only transmission
|
|
950
|
+
|
|
951
|
+
### 🛡️ CSRF Protection
|
|
952
|
+
- **Problem**: Cross-site requests can include cookies
|
|
953
|
+
- **YinzerFlow Solution**: `sameSite: 'strict'` blocks cross-site cookie sends
|
|
954
|
+
|
|
955
|
+
## 🔧 Troubleshooting
|
|
956
|
+
|
|
957
|
+
### Cookies are not being set
|
|
958
|
+
**Symptom**: Cookies don't appear in browser
|
|
959
|
+
|
|
960
|
+
**Solution:**
|
|
961
|
+
- Enable cookie parser in configuration (`enabled: true`)
|
|
962
|
+
- Check `secure: true` only for HTTPS connections
|
|
963
|
+
- Verify domain/path match
|
|
964
|
+
|
|
965
|
+
### Signed cookies return `false` when unsigned
|
|
966
|
+
**Symptom**: `ctx.request.signedCookies` is empty or returns false
|
|
967
|
+
|
|
968
|
+
**Solution:**
|
|
969
|
+
- Verify secret is correct
|
|
970
|
+
- Ensure cookie was properly signed using `ctx.cookies.sign()`
|
|
971
|
+
- Check if cookie was tampered with
|
|
972
|
+
|
|
973
|
+
### Cookies accessible to JavaScript when they shouldn't be
|
|
974
|
+
**Symptom**: JavaScript can read sensitive cookies
|
|
975
|
+
|
|
976
|
+
**Solution:** Set `httpOnly: true` in defaults or per-cookie
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
### Cookies accessible to JavaScript when they shouldn't be
|
|
980
|
+
**Symptom**: JavaScript can read sensitive cookies
|
|
981
|
+
|
|
982
|
+
**Solution:** Set `httpOnly: true` in defaults or per-cookie
|