nextjs-secure 0.1.1 → 0.3.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 CHANGED
@@ -47,6 +47,15 @@ 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)
55
+ - [Security Headers](#security-headers)
56
+ - [Quick Start](#quick-start-1)
57
+ - [Presets](#presets)
58
+ - [Custom Configuration](#custom-configuration)
50
59
  - [Utilities](#utilities)
51
60
  - [API Reference](#api-reference)
52
61
  - [Examples](#examples)
@@ -387,6 +396,240 @@ export async function GET(request: NextRequest) {
387
396
  }
388
397
  ```
389
398
 
399
+ ## CSRF Protection
400
+
401
+ Protect your forms against Cross-Site Request Forgery attacks using the double submit cookie pattern.
402
+
403
+ ### Basic Setup
404
+
405
+ ```typescript
406
+ // app/api/csrf/route.ts - Token endpoint
407
+ import { generateCSRF } from 'nextjs-secure/csrf'
408
+
409
+ export async function GET() {
410
+ const { token, cookieHeader } = await generateCSRF()
411
+
412
+ return Response.json(
413
+ { csrfToken: token },
414
+ { headers: { 'Set-Cookie': cookieHeader } }
415
+ )
416
+ }
417
+ ```
418
+
419
+ ```typescript
420
+ // app/api/submit/route.ts - Protected endpoint
421
+ import { withCSRF } from 'nextjs-secure/csrf'
422
+
423
+ export const POST = withCSRF(async (req) => {
424
+ const data = await req.json()
425
+ // Safe to process - CSRF validated
426
+ return Response.json({ success: true })
427
+ })
428
+ ```
429
+
430
+ ### Client-Side Usage
431
+
432
+ ```typescript
433
+ // Fetch token on page load
434
+ const { csrfToken } = await fetch('/api/csrf').then(r => r.json())
435
+
436
+ // Include in form submissions
437
+ fetch('/api/submit', {
438
+ method: 'POST',
439
+ headers: {
440
+ 'Content-Type': 'application/json',
441
+ 'x-csrf-token': csrfToken // Token in header
442
+ },
443
+ body: JSON.stringify({ data: '...' })
444
+ })
445
+ ```
446
+
447
+ Or include in form body:
448
+
449
+ ```typescript
450
+ fetch('/api/submit', {
451
+ method: 'POST',
452
+ headers: { 'Content-Type': 'application/json' },
453
+ body: JSON.stringify({
454
+ _csrf: csrfToken, // Token in body
455
+ data: '...'
456
+ })
457
+ })
458
+ ```
459
+
460
+ ### Configuration
461
+
462
+ ```typescript
463
+ import { withCSRF } from 'nextjs-secure/csrf'
464
+
465
+ export const POST = withCSRF(handler, {
466
+ // Cookie settings
467
+ cookie: {
468
+ name: '__csrf', // Cookie name
469
+ httpOnly: true, // Not accessible via JS
470
+ secure: true, // HTTPS only
471
+ sameSite: 'strict', // Strict same-site policy
472
+ maxAge: 86400 // 24 hours
473
+ },
474
+
475
+ // Where to look for token
476
+ headerName: 'x-csrf-token', // Header name
477
+ fieldName: '_csrf', // Body field name
478
+
479
+ // Token settings
480
+ secret: process.env.CSRF_SECRET, // Signing secret
481
+ tokenLength: 32, // Token size in bytes
482
+
483
+ // Protected methods (default: POST, PUT, PATCH, DELETE)
484
+ protectedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
485
+
486
+ // Skip protection conditionally
487
+ skip: (req) => req.headers.get('x-api-key') === 'trusted',
488
+
489
+ // Custom error response
490
+ onError: (req, reason) => {
491
+ return new Response(`CSRF failed: ${reason}`, { status: 403 })
492
+ }
493
+ })
494
+ ```
495
+
496
+ ### Manual Validation
497
+
498
+ ```typescript
499
+ import { validateCSRF } from 'nextjs-secure/csrf'
500
+
501
+ export async function POST(req) {
502
+ const result = await validateCSRF(req)
503
+
504
+ if (!result.valid) {
505
+ console.log('CSRF failed:', result.reason)
506
+ // reason: 'missing_cookie' | 'invalid_cookie' | 'missing_token' | 'token_mismatch'
507
+ return Response.json({ error: result.reason }, { status: 403 })
508
+ }
509
+
510
+ // Continue processing
511
+ }
512
+ ```
513
+
514
+ ### Environment Variable
515
+
516
+ Set `CSRF_SECRET` in your environment:
517
+
518
+ ```env
519
+ CSRF_SECRET=your-secret-key-min-32-chars-recommended
520
+ ```
521
+
522
+ ## Security Headers
523
+
524
+ Add security headers to your responses with pre-configured presets or custom configuration.
525
+
526
+ ### Quick Start
527
+
528
+ ```typescript
529
+ import { withSecurityHeaders } from 'nextjs-secure/headers'
530
+
531
+ // Use strict preset (default)
532
+ export const GET = withSecurityHeaders(async (req) => {
533
+ return Response.json({ data: 'protected' })
534
+ })
535
+ ```
536
+
537
+ ### Presets
538
+
539
+ Three presets available: `strict`, `relaxed`, `api`
540
+
541
+ ```typescript
542
+ // Strict: Maximum security (default)
543
+ export const GET = withSecurityHeaders(handler, { preset: 'strict' })
544
+
545
+ // Relaxed: Development-friendly, allows inline scripts
546
+ export const GET = withSecurityHeaders(handler, { preset: 'relaxed' })
547
+
548
+ // API: Optimized for JSON APIs
549
+ export const GET = withSecurityHeaders(handler, { preset: 'api' })
550
+ ```
551
+
552
+ ### Custom Configuration
553
+
554
+ ```typescript
555
+ import { withSecurityHeaders } from 'nextjs-secure/headers'
556
+
557
+ export const GET = withSecurityHeaders(handler, {
558
+ config: {
559
+ // Content-Security-Policy
560
+ contentSecurityPolicy: {
561
+ defaultSrc: ["'self'"],
562
+ scriptSrc: ["'self'", "'unsafe-inline'"],
563
+ styleSrc: ["'self'", "'unsafe-inline'"],
564
+ imgSrc: ["'self'", 'data:', 'https:'],
565
+ },
566
+
567
+ // Strict-Transport-Security
568
+ strictTransportSecurity: {
569
+ maxAge: 31536000,
570
+ includeSubDomains: true,
571
+ preload: true,
572
+ },
573
+
574
+ // Other headers
575
+ xFrameOptions: 'DENY', // or 'SAMEORIGIN'
576
+ xContentTypeOptions: true, // X-Content-Type-Options: nosniff
577
+ referrerPolicy: 'strict-origin-when-cross-origin',
578
+
579
+ // Cross-Origin headers
580
+ crossOriginOpenerPolicy: 'same-origin',
581
+ crossOriginEmbedderPolicy: 'require-corp',
582
+ crossOriginResourcePolicy: 'same-origin',
583
+
584
+ // Permissions-Policy (disable features)
585
+ permissionsPolicy: {
586
+ camera: [],
587
+ microphone: [],
588
+ geolocation: [],
589
+ },
590
+ }
591
+ })
592
+ ```
593
+
594
+ ### Disable Specific Headers
595
+
596
+ ```typescript
597
+ export const GET = withSecurityHeaders(handler, {
598
+ config: {
599
+ contentSecurityPolicy: false, // Disable CSP
600
+ xFrameOptions: false, // Disable X-Frame-Options
601
+ }
602
+ })
603
+ ```
604
+
605
+ ### Manual Header Creation
606
+
607
+ ```typescript
608
+ import { createSecurityHeaders } from 'nextjs-secure/headers'
609
+
610
+ export async function GET() {
611
+ const headers = createSecurityHeaders({ preset: 'api' })
612
+
613
+ return new Response(JSON.stringify({ ok: true }), {
614
+ headers,
615
+ })
616
+ }
617
+ ```
618
+
619
+ ### Available Headers
620
+
621
+ | Header | Description |
622
+ |--------|-------------|
623
+ | Content-Security-Policy | Controls resources the page can load |
624
+ | Strict-Transport-Security | Forces HTTPS connections |
625
+ | X-Frame-Options | Prevents clickjacking |
626
+ | X-Content-Type-Options | Prevents MIME sniffing |
627
+ | Referrer-Policy | Controls referrer information |
628
+ | Permissions-Policy | Disables browser features |
629
+ | Cross-Origin-Opener-Policy | Isolates browsing context |
630
+ | Cross-Origin-Embedder-Policy | Controls embedding |
631
+ | Cross-Origin-Resource-Policy | Controls resource sharing |
632
+
390
633
  ## Utilities
391
634
 
392
635
  ### Duration Parsing
package/dist/csrf.cjs CHANGED
@@ -1,18 +1,195 @@
1
1
  'use strict';
2
2
 
3
- // src/middleware/csrf/index.ts
4
- function withCsrf() {
5
- throw new Error("CSRF middleware coming soon in v0.2.0");
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 generateCsrfToken() {
8
- throw new Error("CSRF middleware coming soon in v0.2.0");
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 validateCsrfToken() {
11
- throw new Error("CSRF middleware coming soon in v0.2.0");
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.generateCsrfToken = generateCsrfToken;
15
- exports.validateCsrfToken = validateCsrfToken;
16
- exports.withCsrf = withCsrf;
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 Protection Middleware (Coming Soon)
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
- * // Validate token
15
- * export const POST = withCsrf(async (req) => {
16
- * return Response.json({ ok: true })
17
- * })
18
- * ```
19
- *
20
- * @packageDocumentation
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 withCsrf(): void;
23
- declare function generateCsrfToken(): void;
24
- declare function validateCsrfToken(): void;
73
+ declare function tokensMatch(a: string, b: string): boolean;
25
74
 
26
- export { generateCsrfToken, validateCsrfToken, withCsrf };
75
+ export { type CSRFConfig, type CSRFCookieOptions, type CSRFToken, createToken, generateCSRF, tokensMatch, validateCSRF, verifyToken, withCSRF };