nextjs-secure 0.1.1 → 0.2.0
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/README.md +128 -0
- package/dist/csrf.cjs +187 -10
- package/dist/csrf.cjs.map +1 -1
- package/dist/csrf.d.cts +71 -22
- package/dist/csrf.d.ts +71 -22
- package/dist/csrf.js +182 -8
- package/dist/csrf.js.map +1 -1
- package/dist/index.cjs +190 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +185 -2
- package/dist/index.js.map +1 -1
- package/dist/{memory-g9zyQPy5.d.cts → memory-Dauy-IH3.d.cts} +1 -1
- package/dist/{memory-g9zyQPy5.d.ts → memory-Dauy-IH3.d.ts} +1 -1
- package/dist/rate-limit.d.cts +2 -2
- package/dist/rate-limit.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,6 +47,11 @@ pnpm add nextjs-secure
|
|
|
47
47
|
- [Storage Backends](#storage-backends)
|
|
48
48
|
- [Custom Identifiers](#custom-identifiers)
|
|
49
49
|
- [Response Customization](#response-customization)
|
|
50
|
+
- [CSRF Protection](#csrf-protection)
|
|
51
|
+
- [Basic Setup](#basic-setup)
|
|
52
|
+
- [Client-Side Usage](#client-side-usage)
|
|
53
|
+
- [Configuration](#configuration-1)
|
|
54
|
+
- [Manual Validation](#manual-validation)
|
|
50
55
|
- [Utilities](#utilities)
|
|
51
56
|
- [API Reference](#api-reference)
|
|
52
57
|
- [Examples](#examples)
|
|
@@ -387,6 +392,129 @@ export async function GET(request: NextRequest) {
|
|
|
387
392
|
}
|
|
388
393
|
```
|
|
389
394
|
|
|
395
|
+
## CSRF Protection
|
|
396
|
+
|
|
397
|
+
Protect your forms against Cross-Site Request Forgery attacks using the double submit cookie pattern.
|
|
398
|
+
|
|
399
|
+
### Basic Setup
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// app/api/csrf/route.ts - Token endpoint
|
|
403
|
+
import { generateCSRF } from 'nextjs-secure/csrf'
|
|
404
|
+
|
|
405
|
+
export async function GET() {
|
|
406
|
+
const { token, cookieHeader } = await generateCSRF()
|
|
407
|
+
|
|
408
|
+
return Response.json(
|
|
409
|
+
{ csrfToken: token },
|
|
410
|
+
{ headers: { 'Set-Cookie': cookieHeader } }
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// app/api/submit/route.ts - Protected endpoint
|
|
417
|
+
import { withCSRF } from 'nextjs-secure/csrf'
|
|
418
|
+
|
|
419
|
+
export const POST = withCSRF(async (req) => {
|
|
420
|
+
const data = await req.json()
|
|
421
|
+
// Safe to process - CSRF validated
|
|
422
|
+
return Response.json({ success: true })
|
|
423
|
+
})
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### Client-Side Usage
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
// Fetch token on page load
|
|
430
|
+
const { csrfToken } = await fetch('/api/csrf').then(r => r.json())
|
|
431
|
+
|
|
432
|
+
// Include in form submissions
|
|
433
|
+
fetch('/api/submit', {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers: {
|
|
436
|
+
'Content-Type': 'application/json',
|
|
437
|
+
'x-csrf-token': csrfToken // Token in header
|
|
438
|
+
},
|
|
439
|
+
body: JSON.stringify({ data: '...' })
|
|
440
|
+
})
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
Or include in form body:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
fetch('/api/submit', {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
headers: { 'Content-Type': 'application/json' },
|
|
449
|
+
body: JSON.stringify({
|
|
450
|
+
_csrf: csrfToken, // Token in body
|
|
451
|
+
data: '...'
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Configuration
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
import { withCSRF } from 'nextjs-secure/csrf'
|
|
460
|
+
|
|
461
|
+
export const POST = withCSRF(handler, {
|
|
462
|
+
// Cookie settings
|
|
463
|
+
cookie: {
|
|
464
|
+
name: '__csrf', // Cookie name
|
|
465
|
+
httpOnly: true, // Not accessible via JS
|
|
466
|
+
secure: true, // HTTPS only
|
|
467
|
+
sameSite: 'strict', // Strict same-site policy
|
|
468
|
+
maxAge: 86400 // 24 hours
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
// Where to look for token
|
|
472
|
+
headerName: 'x-csrf-token', // Header name
|
|
473
|
+
fieldName: '_csrf', // Body field name
|
|
474
|
+
|
|
475
|
+
// Token settings
|
|
476
|
+
secret: process.env.CSRF_SECRET, // Signing secret
|
|
477
|
+
tokenLength: 32, // Token size in bytes
|
|
478
|
+
|
|
479
|
+
// Protected methods (default: POST, PUT, PATCH, DELETE)
|
|
480
|
+
protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
|
481
|
+
|
|
482
|
+
// Skip protection conditionally
|
|
483
|
+
skip: (req) => req.headers.get('x-api-key') === 'trusted',
|
|
484
|
+
|
|
485
|
+
// Custom error response
|
|
486
|
+
onError: (req, reason) => {
|
|
487
|
+
return new Response(`CSRF failed: ${reason}`, { status: 403 })
|
|
488
|
+
}
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Manual Validation
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
import { validateCSRF } from 'nextjs-secure/csrf'
|
|
496
|
+
|
|
497
|
+
export async function POST(req) {
|
|
498
|
+
const result = await validateCSRF(req)
|
|
499
|
+
|
|
500
|
+
if (!result.valid) {
|
|
501
|
+
console.log('CSRF failed:', result.reason)
|
|
502
|
+
// reason: 'missing_cookie' | 'invalid_cookie' | 'missing_token' | 'token_mismatch'
|
|
503
|
+
return Response.json({ error: result.reason }, { status: 403 })
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Continue processing
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Environment Variable
|
|
511
|
+
|
|
512
|
+
Set `CSRF_SECRET` in your environment:
|
|
513
|
+
|
|
514
|
+
```env
|
|
515
|
+
CSRF_SECRET=your-secret-key-min-32-chars-recommended
|
|
516
|
+
```
|
|
517
|
+
|
|
390
518
|
## Utilities
|
|
391
519
|
|
|
392
520
|
### Duration Parsing
|
package/dist/csrf.cjs
CHANGED
|
@@ -1,18 +1,195 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/middleware/csrf/token.ts
|
|
6
|
+
var encoder = new TextEncoder();
|
|
7
|
+
function randomBytes(length) {
|
|
8
|
+
const bytes = new Uint8Array(length);
|
|
9
|
+
crypto.webcrypto.getRandomValues(bytes);
|
|
10
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
11
|
+
}
|
|
12
|
+
async function createSignature(data, secret) {
|
|
13
|
+
const key = await crypto.webcrypto.subtle.importKey(
|
|
14
|
+
"raw",
|
|
15
|
+
encoder.encode(secret),
|
|
16
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
17
|
+
false,
|
|
18
|
+
["sign"]
|
|
19
|
+
);
|
|
20
|
+
const sig = await crypto.webcrypto.subtle.sign("HMAC", key, encoder.encode(data));
|
|
21
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
22
|
+
}
|
|
23
|
+
function safeCompare(a, b) {
|
|
24
|
+
if (a.length !== b.length) return false;
|
|
25
|
+
let result = 0;
|
|
26
|
+
for (let i = 0; i < a.length; i++) {
|
|
27
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
28
|
+
}
|
|
29
|
+
return result === 0;
|
|
30
|
+
}
|
|
31
|
+
async function createToken(secret, length = 32) {
|
|
32
|
+
const data = randomBytes(length);
|
|
33
|
+
const sig = await createSignature(data, secret);
|
|
34
|
+
return `${data}.${sig}`;
|
|
35
|
+
}
|
|
36
|
+
async function verifyToken(token, secret) {
|
|
37
|
+
if (!token || typeof token !== "string") return false;
|
|
38
|
+
const parts = token.split(".");
|
|
39
|
+
if (parts.length !== 2) return false;
|
|
40
|
+
const [data, sig] = parts;
|
|
41
|
+
if (!data || !sig) return false;
|
|
42
|
+
try {
|
|
43
|
+
const expected = await createSignature(data, secret);
|
|
44
|
+
return safeCompare(sig, expected);
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function tokensMatch(a, b) {
|
|
50
|
+
if (!a || !b) return false;
|
|
51
|
+
return safeCompare(a, b);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/middleware/csrf/middleware.ts
|
|
55
|
+
var DEFAULT_COOKIE = {
|
|
56
|
+
name: "__csrf",
|
|
57
|
+
path: "/",
|
|
58
|
+
httpOnly: true,
|
|
59
|
+
secure: process.env.NODE_ENV === "production",
|
|
60
|
+
sameSite: "strict",
|
|
61
|
+
maxAge: 86400
|
|
62
|
+
// 24h
|
|
63
|
+
};
|
|
64
|
+
var DEFAULT_CONFIG = {
|
|
65
|
+
headerName: "x-csrf-token",
|
|
66
|
+
fieldName: "_csrf",
|
|
67
|
+
tokenLength: 32,
|
|
68
|
+
protectedMethods: ["POST", "PUT", "PATCH", "DELETE"]
|
|
69
|
+
};
|
|
70
|
+
function getSecret(config) {
|
|
71
|
+
const secret = config.secret || process.env.CSRF_SECRET;
|
|
72
|
+
if (!secret) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"CSRF secret is required. Set config.secret or CSRF_SECRET env variable."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return secret;
|
|
78
|
+
}
|
|
79
|
+
function buildCookieString(name, value, opts) {
|
|
80
|
+
let cookie = `${name}=${value}`;
|
|
81
|
+
if (opts.path) cookie += `; Path=${opts.path}`;
|
|
82
|
+
if (opts.domain) cookie += `; Domain=${opts.domain}`;
|
|
83
|
+
if (opts.maxAge) cookie += `; Max-Age=${opts.maxAge}`;
|
|
84
|
+
if (opts.httpOnly) cookie += "; HttpOnly";
|
|
85
|
+
if (opts.secure) cookie += "; Secure";
|
|
86
|
+
if (opts.sameSite) cookie += `; SameSite=${opts.sameSite}`;
|
|
87
|
+
return cookie;
|
|
88
|
+
}
|
|
89
|
+
async function extractToken(req, headerName, fieldName) {
|
|
90
|
+
const headerToken = req.headers.get(headerName);
|
|
91
|
+
if (headerToken) return headerToken;
|
|
92
|
+
const contentType = req.headers.get("content-type") || "";
|
|
93
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
94
|
+
try {
|
|
95
|
+
const cloned = req.clone();
|
|
96
|
+
const formData = await cloned.formData();
|
|
97
|
+
const token = formData.get(fieldName);
|
|
98
|
+
if (typeof token === "string") return token;
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (contentType.includes("application/json")) {
|
|
103
|
+
try {
|
|
104
|
+
const cloned = req.clone();
|
|
105
|
+
const body = await cloned.json();
|
|
106
|
+
if (body && typeof body[fieldName] === "string") {
|
|
107
|
+
return body[fieldName];
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function defaultErrorResponse(_req, reason) {
|
|
115
|
+
return new Response(JSON.stringify({ error: "CSRF validation failed", reason }), {
|
|
116
|
+
status: 403,
|
|
117
|
+
headers: { "Content-Type": "application/json" }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function withCSRF(handler, config = {}) {
|
|
121
|
+
const secret = getSecret(config);
|
|
122
|
+
const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
|
|
123
|
+
const headerName = config.headerName || DEFAULT_CONFIG.headerName;
|
|
124
|
+
const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName;
|
|
125
|
+
const protectedMethods = config.protectedMethods || DEFAULT_CONFIG.protectedMethods;
|
|
126
|
+
const onError = config.onError || defaultErrorResponse;
|
|
127
|
+
return async (req) => {
|
|
128
|
+
const method = req.method.toUpperCase();
|
|
129
|
+
if (!protectedMethods.includes(method)) {
|
|
130
|
+
return handler(req);
|
|
131
|
+
}
|
|
132
|
+
if (config.skip) {
|
|
133
|
+
const shouldSkip = await config.skip(req);
|
|
134
|
+
if (shouldSkip) return handler(req);
|
|
135
|
+
}
|
|
136
|
+
const cookieName = cookieOpts.name || "__csrf";
|
|
137
|
+
const cookieToken = req.cookies.get(cookieName)?.value;
|
|
138
|
+
if (!cookieToken) {
|
|
139
|
+
return onError(req, "missing_cookie");
|
|
140
|
+
}
|
|
141
|
+
const cookieValid = await verifyToken(cookieToken, secret);
|
|
142
|
+
if (!cookieValid) {
|
|
143
|
+
return onError(req, "invalid_cookie");
|
|
144
|
+
}
|
|
145
|
+
const requestToken = await extractToken(req, headerName, fieldName);
|
|
146
|
+
if (!requestToken) {
|
|
147
|
+
return onError(req, "missing_token");
|
|
148
|
+
}
|
|
149
|
+
if (!tokensMatch(cookieToken, requestToken)) {
|
|
150
|
+
return onError(req, "token_mismatch");
|
|
151
|
+
}
|
|
152
|
+
return handler(req);
|
|
153
|
+
};
|
|
6
154
|
}
|
|
7
|
-
function
|
|
8
|
-
|
|
155
|
+
async function generateCSRF(config = {}) {
|
|
156
|
+
const secret = getSecret(config);
|
|
157
|
+
const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
|
|
158
|
+
const tokenLength = config.tokenLength || DEFAULT_CONFIG.tokenLength;
|
|
159
|
+
const cookieName = cookieOpts.name || "__csrf";
|
|
160
|
+
const token = await createToken(secret, tokenLength);
|
|
161
|
+
const cookieHeader = buildCookieString(cookieName, token, cookieOpts);
|
|
162
|
+
return { token, cookieHeader };
|
|
9
163
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
164
|
+
async function validateCSRF(req, config = {}) {
|
|
165
|
+
const secret = getSecret(config);
|
|
166
|
+
const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie };
|
|
167
|
+
const headerName = config.headerName || DEFAULT_CONFIG.headerName;
|
|
168
|
+
const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName;
|
|
169
|
+
const cookieName = cookieOpts.name || "__csrf";
|
|
170
|
+
const cookieToken = req.cookies.get(cookieName)?.value;
|
|
171
|
+
if (!cookieToken) {
|
|
172
|
+
return { valid: false, reason: "missing_cookie" };
|
|
173
|
+
}
|
|
174
|
+
const cookieValid = await verifyToken(cookieToken, secret);
|
|
175
|
+
if (!cookieValid) {
|
|
176
|
+
return { valid: false, reason: "invalid_cookie" };
|
|
177
|
+
}
|
|
178
|
+
const requestToken = await extractToken(req, headerName, fieldName);
|
|
179
|
+
if (!requestToken) {
|
|
180
|
+
return { valid: false, reason: "missing_token" };
|
|
181
|
+
}
|
|
182
|
+
if (!tokensMatch(cookieToken, requestToken)) {
|
|
183
|
+
return { valid: false, reason: "token_mismatch" };
|
|
184
|
+
}
|
|
185
|
+
return { valid: true };
|
|
12
186
|
}
|
|
13
187
|
|
|
14
|
-
exports.
|
|
15
|
-
exports.
|
|
16
|
-
exports.
|
|
188
|
+
exports.createToken = createToken;
|
|
189
|
+
exports.generateCSRF = generateCSRF;
|
|
190
|
+
exports.tokensMatch = tokensMatch;
|
|
191
|
+
exports.validateCSRF = validateCSRF;
|
|
192
|
+
exports.verifyToken = verifyToken;
|
|
193
|
+
exports.withCSRF = withCSRF;
|
|
17
194
|
//# sourceMappingURL=csrf.cjs.map
|
|
18
195
|
//# sourceMappingURL=csrf.cjs.map
|
package/dist/csrf.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware/csrf/index.ts"],"names":[],"mappings":";;;AAuBO,SAAS,QAAA,GAAW;AACzB,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD;AAEO,SAAS,iBAAA,GAAoB;AAClC,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD;AAEO,SAAS,iBAAA,GAAoB;AAClC,EAAA,MAAM,IAAI,MAAM,uCAAuC,CAAA;AACzD","file":"csrf.cjs","sourcesContent":["/**\n * CSRF Protection Middleware (Coming Soon)\n *\n * @example\n * ```typescript\n * import { withCsrf, generateCsrfToken } from 'next-secure/csrf'\n *\n * // Generate token\n * export async function GET() {\n * const token = await generateCsrfToken()\n * return Response.json({ csrfToken: token })\n * }\n *\n * // Validate token\n * export const POST = withCsrf(async (req) => {\n * return Response.json({ ok: true })\n * })\n * ```\n *\n * @packageDocumentation\n */\n\n// Placeholder for CSRF middleware\nexport function withCsrf() {\n throw new Error('CSRF middleware coming soon in v0.2.0')\n}\n\nexport function generateCsrfToken() {\n throw new Error('CSRF middleware coming soon in v0.2.0')\n}\n\nexport function validateCsrfToken() {\n throw new Error('CSRF middleware coming soon in v0.2.0')\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware/csrf/token.ts","../src/middleware/csrf/middleware.ts"],"names":["webcrypto"],"mappings":"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","file":"csrf.cjs","sourcesContent":["import { webcrypto } from 'node:crypto'\n\nconst encoder = new TextEncoder()\n\n/**\n * Generate random bytes as hex string\n */\nexport function randomBytes(length: number): string {\n const bytes = new Uint8Array(length)\n webcrypto.getRandomValues(bytes)\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Create HMAC signature\n */\nasync function createSignature(data: string, secret: string): Promise<string> {\n const key = await webcrypto.subtle.importKey(\n 'raw',\n encoder.encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n\n const sig = await webcrypto.subtle.sign('HMAC', key, encoder.encode(data))\n return Array.from(new Uint8Array(sig))\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/**\n * Constant-time string comparison to prevent timing attacks\n */\nfunction safeCompare(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n\n let result = 0\n for (let i = 0; i < a.length; i++) {\n result |= a.charCodeAt(i) ^ b.charCodeAt(i)\n }\n return result === 0\n}\n\n/**\n * Create a signed CSRF token\n */\nexport async function createToken(\n secret: string,\n length: number = 32\n): Promise<string> {\n const data = randomBytes(length)\n const sig = await createSignature(data, secret)\n return `${data}.${sig}`\n}\n\n/**\n * Verify a signed CSRF token\n */\nexport async function verifyToken(\n token: string,\n secret: string\n): Promise<boolean> {\n if (!token || typeof token !== 'string') return false\n\n const parts = token.split('.')\n if (parts.length !== 2) return false\n\n const [data, sig] = parts\n if (!data || !sig) return false\n\n try {\n const expected = await createSignature(data, secret)\n return safeCompare(sig, expected)\n } catch {\n return false\n }\n}\n\n/**\n * Compare two tokens (constant-time)\n */\nexport function tokensMatch(a: string, b: string): boolean {\n if (!a || !b) return false\n return safeCompare(a, b)\n}\n","import type { NextRequest } from 'next/server'\nimport type { CSRFConfig, CSRFCookieOptions } from './types'\nimport { createToken, verifyToken, tokensMatch } from './token'\n\ntype RouteHandler = (req: NextRequest) => Response | Promise<Response>\n\nconst DEFAULT_COOKIE: CSRFCookieOptions = {\n name: '__csrf',\n path: '/',\n httpOnly: true,\n secure: process.env.NODE_ENV === 'production',\n sameSite: 'strict',\n maxAge: 86400, // 24h\n}\n\nconst DEFAULT_CONFIG: Required<Omit<CSRFConfig, 'skip' | 'onError'>> = {\n cookie: DEFAULT_COOKIE,\n headerName: 'x-csrf-token',\n fieldName: '_csrf',\n secret: '',\n tokenLength: 32,\n protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],\n}\n\nfunction getSecret(config: CSRFConfig): string {\n const secret = config.secret || process.env.CSRF_SECRET\n if (!secret) {\n throw new Error(\n 'CSRF secret is required. Set config.secret or CSRF_SECRET env variable.'\n )\n }\n return secret\n}\n\nfunction buildCookieString(name: string, value: string, opts: CSRFCookieOptions): string {\n let cookie = `${name}=${value}`\n\n if (opts.path) cookie += `; Path=${opts.path}`\n if (opts.domain) cookie += `; Domain=${opts.domain}`\n if (opts.maxAge) cookie += `; Max-Age=${opts.maxAge}`\n if (opts.httpOnly) cookie += '; HttpOnly'\n if (opts.secure) cookie += '; Secure'\n if (opts.sameSite) cookie += `; SameSite=${opts.sameSite}`\n\n return cookie\n}\n\n/**\n * Extract token from request (header or body)\n */\nasync function extractToken(\n req: NextRequest,\n headerName: string,\n fieldName: string\n): Promise<string | null> {\n // check header first\n const headerToken = req.headers.get(headerName)\n if (headerToken) return headerToken\n\n // try to get from form data\n const contentType = req.headers.get('content-type') || ''\n\n if (contentType.includes('application/x-www-form-urlencoded')) {\n try {\n const cloned = req.clone()\n const formData = await cloned.formData()\n const token = formData.get(fieldName)\n if (typeof token === 'string') return token\n } catch {\n // ignore parse errors\n }\n }\n\n if (contentType.includes('application/json')) {\n try {\n const cloned = req.clone()\n const body = await cloned.json()\n if (body && typeof body[fieldName] === 'string') {\n return body[fieldName]\n }\n } catch {\n // ignore parse errors\n }\n }\n\n return null\n}\n\nfunction defaultErrorResponse(_req: NextRequest, reason: string): Response {\n return new Response(JSON.stringify({ error: 'CSRF validation failed', reason }), {\n status: 403,\n headers: { 'Content-Type': 'application/json' },\n })\n}\n\n/**\n * CSRF protection middleware\n *\n * Uses double submit cookie pattern:\n * 1. Server sets a signed token in a cookie\n * 2. Client sends the same token in header/body\n * 3. Server compares both values\n */\nexport function withCSRF(handler: RouteHandler, config: CSRFConfig = {}): RouteHandler {\n const secret = getSecret(config)\n const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie }\n const headerName = config.headerName || DEFAULT_CONFIG.headerName\n const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName\n const protectedMethods = config.protectedMethods || DEFAULT_CONFIG.protectedMethods\n const onError = config.onError || defaultErrorResponse\n\n return async (req: NextRequest): Promise<Response> => {\n const method = req.method.toUpperCase()\n\n // skip unprotected methods\n if (!protectedMethods.includes(method)) {\n return handler(req)\n }\n\n // custom skip logic\n if (config.skip) {\n const shouldSkip = await config.skip(req)\n if (shouldSkip) return handler(req)\n }\n\n const cookieName = cookieOpts.name || '__csrf'\n const cookieToken = req.cookies.get(cookieName)?.value\n\n // no cookie = first request, reject\n if (!cookieToken) {\n return onError(req, 'missing_cookie')\n }\n\n // verify cookie token is valid (signed by us)\n const cookieValid = await verifyToken(cookieToken, secret)\n if (!cookieValid) {\n return onError(req, 'invalid_cookie')\n }\n\n // get token from request\n const requestToken = await extractToken(req, headerName, fieldName)\n if (!requestToken) {\n return onError(req, 'missing_token')\n }\n\n // compare tokens\n if (!tokensMatch(cookieToken, requestToken)) {\n return onError(req, 'token_mismatch')\n }\n\n return handler(req)\n }\n}\n\n/**\n * Generate a new CSRF token and cookie header\n * Use this in GET routes to set the initial token\n */\nexport async function generateCSRF(config: CSRFConfig = {}): Promise<{\n token: string\n cookieHeader: string\n}> {\n const secret = getSecret(config)\n const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie }\n const tokenLength = config.tokenLength || DEFAULT_CONFIG.tokenLength\n const cookieName = cookieOpts.name || '__csrf'\n\n const token = await createToken(secret, tokenLength)\n const cookieHeader = buildCookieString(cookieName, token, cookieOpts)\n\n return { token, cookieHeader }\n}\n\n/**\n * Validate a CSRF token without middleware\n * Useful for custom validation flows\n */\nexport async function validateCSRF(\n req: NextRequest,\n config: CSRFConfig = {}\n): Promise<{ valid: boolean; reason?: string }> {\n const secret = getSecret(config)\n const cookieOpts = { ...DEFAULT_COOKIE, ...config.cookie }\n const headerName = config.headerName || DEFAULT_CONFIG.headerName\n const fieldName = config.fieldName || DEFAULT_CONFIG.fieldName\n const cookieName = cookieOpts.name || '__csrf'\n\n const cookieToken = req.cookies.get(cookieName)?.value\n if (!cookieToken) {\n return { valid: false, reason: 'missing_cookie' }\n }\n\n const cookieValid = await verifyToken(cookieToken, secret)\n if (!cookieValid) {\n return { valid: false, reason: 'invalid_cookie' }\n }\n\n const requestToken = await extractToken(req, headerName, fieldName)\n if (!requestToken) {\n return { valid: false, reason: 'missing_token' }\n }\n\n if (!tokensMatch(cookieToken, requestToken)) {\n return { valid: false, reason: 'token_mismatch' }\n }\n\n return { valid: true }\n}\n"]}
|
package/dist/csrf.d.cts
CHANGED
|
@@ -1,26 +1,75 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
interface CSRFCookieOptions {
|
|
4
|
+
name?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
domain?: string;
|
|
7
|
+
secure?: boolean;
|
|
8
|
+
httpOnly?: boolean;
|
|
9
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
10
|
+
maxAge?: number;
|
|
11
|
+
}
|
|
12
|
+
interface CSRFConfig {
|
|
13
|
+
/** Cookie settings */
|
|
14
|
+
cookie?: CSRFCookieOptions;
|
|
15
|
+
/** Header name to check for token (default: x-csrf-token) */
|
|
16
|
+
headerName?: string;
|
|
17
|
+
/** Form field name (default: _csrf) */
|
|
18
|
+
fieldName?: string;
|
|
19
|
+
/** Secret for signing tokens */
|
|
20
|
+
secret?: string;
|
|
21
|
+
/** Token length in bytes (default: 32) */
|
|
22
|
+
tokenLength?: number;
|
|
23
|
+
/** Methods to protect (default: POST, PUT, PATCH, DELETE) */
|
|
24
|
+
protectedMethods?: string[];
|
|
25
|
+
/** Skip CSRF check for specific requests */
|
|
26
|
+
skip?: (req: NextRequest) => boolean | Promise<boolean>;
|
|
27
|
+
/** Called when CSRF validation fails */
|
|
28
|
+
onError?: (req: NextRequest, reason: string) => Response | Promise<Response>;
|
|
29
|
+
}
|
|
30
|
+
interface CSRFToken {
|
|
31
|
+
value: string;
|
|
32
|
+
cookie: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type RouteHandler = (req: NextRequest) => Response | Promise<Response>;
|
|
1
36
|
/**
|
|
2
|
-
* CSRF
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* import { withCsrf, generateCsrfToken } from 'next-secure/csrf'
|
|
7
|
-
*
|
|
8
|
-
* // Generate token
|
|
9
|
-
* export async function GET() {
|
|
10
|
-
* const token = await generateCsrfToken()
|
|
11
|
-
* return Response.json({ csrfToken: token })
|
|
12
|
-
* }
|
|
37
|
+
* CSRF protection middleware
|
|
13
38
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
* Uses double submit cookie pattern:
|
|
40
|
+
* 1. Server sets a signed token in a cookie
|
|
41
|
+
* 2. Client sends the same token in header/body
|
|
42
|
+
* 3. Server compares both values
|
|
43
|
+
*/
|
|
44
|
+
declare function withCSRF(handler: RouteHandler, config?: CSRFConfig): RouteHandler;
|
|
45
|
+
/**
|
|
46
|
+
* Generate a new CSRF token and cookie header
|
|
47
|
+
* Use this in GET routes to set the initial token
|
|
48
|
+
*/
|
|
49
|
+
declare function generateCSRF(config?: CSRFConfig): Promise<{
|
|
50
|
+
token: string;
|
|
51
|
+
cookieHeader: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Validate a CSRF token without middleware
|
|
55
|
+
* Useful for custom validation flows
|
|
56
|
+
*/
|
|
57
|
+
declare function validateCSRF(req: NextRequest, config?: CSRFConfig): Promise<{
|
|
58
|
+
valid: boolean;
|
|
59
|
+
reason?: string;
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a signed CSRF token
|
|
64
|
+
*/
|
|
65
|
+
declare function createToken(secret: string, length?: number): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Verify a signed CSRF token
|
|
68
|
+
*/
|
|
69
|
+
declare function verifyToken(token: string, secret: string): Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Compare two tokens (constant-time)
|
|
21
72
|
*/
|
|
22
|
-
declare function
|
|
23
|
-
declare function generateCsrfToken(): void;
|
|
24
|
-
declare function validateCsrfToken(): void;
|
|
73
|
+
declare function tokensMatch(a: string, b: string): boolean;
|
|
25
74
|
|
|
26
|
-
export {
|
|
75
|
+
export { type CSRFConfig, type CSRFCookieOptions, type CSRFToken, createToken, generateCSRF, tokensMatch, validateCSRF, verifyToken, withCSRF };
|
package/dist/csrf.d.ts
CHANGED
|
@@ -1,26 +1,75 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
|
|
3
|
+
interface CSRFCookieOptions {
|
|
4
|
+
name?: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
domain?: string;
|
|
7
|
+
secure?: boolean;
|
|
8
|
+
httpOnly?: boolean;
|
|
9
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
10
|
+
maxAge?: number;
|
|
11
|
+
}
|
|
12
|
+
interface CSRFConfig {
|
|
13
|
+
/** Cookie settings */
|
|
14
|
+
cookie?: CSRFCookieOptions;
|
|
15
|
+
/** Header name to check for token (default: x-csrf-token) */
|
|
16
|
+
headerName?: string;
|
|
17
|
+
/** Form field name (default: _csrf) */
|
|
18
|
+
fieldName?: string;
|
|
19
|
+
/** Secret for signing tokens */
|
|
20
|
+
secret?: string;
|
|
21
|
+
/** Token length in bytes (default: 32) */
|
|
22
|
+
tokenLength?: number;
|
|
23
|
+
/** Methods to protect (default: POST, PUT, PATCH, DELETE) */
|
|
24
|
+
protectedMethods?: string[];
|
|
25
|
+
/** Skip CSRF check for specific requests */
|
|
26
|
+
skip?: (req: NextRequest) => boolean | Promise<boolean>;
|
|
27
|
+
/** Called when CSRF validation fails */
|
|
28
|
+
onError?: (req: NextRequest, reason: string) => Response | Promise<Response>;
|
|
29
|
+
}
|
|
30
|
+
interface CSRFToken {
|
|
31
|
+
value: string;
|
|
32
|
+
cookie: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type RouteHandler = (req: NextRequest) => Response | Promise<Response>;
|
|
1
36
|
/**
|
|
2
|
-
* CSRF
|
|
3
|
-
*
|
|
4
|
-
* @example
|
|
5
|
-
* ```typescript
|
|
6
|
-
* import { withCsrf, generateCsrfToken } from 'next-secure/csrf'
|
|
7
|
-
*
|
|
8
|
-
* // Generate token
|
|
9
|
-
* export async function GET() {
|
|
10
|
-
* const token = await generateCsrfToken()
|
|
11
|
-
* return Response.json({ csrfToken: token })
|
|
12
|
-
* }
|
|
37
|
+
* CSRF protection middleware
|
|
13
38
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
39
|
+
* Uses double submit cookie pattern:
|
|
40
|
+
* 1. Server sets a signed token in a cookie
|
|
41
|
+
* 2. Client sends the same token in header/body
|
|
42
|
+
* 3. Server compares both values
|
|
43
|
+
*/
|
|
44
|
+
declare function withCSRF(handler: RouteHandler, config?: CSRFConfig): RouteHandler;
|
|
45
|
+
/**
|
|
46
|
+
* Generate a new CSRF token and cookie header
|
|
47
|
+
* Use this in GET routes to set the initial token
|
|
48
|
+
*/
|
|
49
|
+
declare function generateCSRF(config?: CSRFConfig): Promise<{
|
|
50
|
+
token: string;
|
|
51
|
+
cookieHeader: string;
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Validate a CSRF token without middleware
|
|
55
|
+
* Useful for custom validation flows
|
|
56
|
+
*/
|
|
57
|
+
declare function validateCSRF(req: NextRequest, config?: CSRFConfig): Promise<{
|
|
58
|
+
valid: boolean;
|
|
59
|
+
reason?: string;
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a signed CSRF token
|
|
64
|
+
*/
|
|
65
|
+
declare function createToken(secret: string, length?: number): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Verify a signed CSRF token
|
|
68
|
+
*/
|
|
69
|
+
declare function verifyToken(token: string, secret: string): Promise<boolean>;
|
|
70
|
+
/**
|
|
71
|
+
* Compare two tokens (constant-time)
|
|
21
72
|
*/
|
|
22
|
-
declare function
|
|
23
|
-
declare function generateCsrfToken(): void;
|
|
24
|
-
declare function validateCsrfToken(): void;
|
|
73
|
+
declare function tokensMatch(a: string, b: string): boolean;
|
|
25
74
|
|
|
26
|
-
export {
|
|
75
|
+
export { type CSRFConfig, type CSRFCookieOptions, type CSRFToken, createToken, generateCSRF, tokensMatch, validateCSRF, verifyToken, withCSRF };
|