CSRF protection middleware for Next.js applications
npm install @csrf-armor/nextjs





Complete CSRF protection for Next.js applications with App Router and Pages Router support, middleware integration, and React hooks.
Built for Next.js 12+ with support for both App Router and Pages Router, Edge Runtime compatibility, and modern React patterns.
- Features
- Quick Start
- Routing System Setup
- Context Provider Setup (App Router)
- Context Provider Setup (Pages Router)
- Usage in Components
- API Route Example
- Security Best Practices
- 🛡️ Multiple Security Strategies - Choose from 5 different CSRF protection methods
- 🔄 App Router & Pages Router - Full support for both Next.js routing systems
- 🪝 React Hooks - useCsrf hook for seamless client-side integration
- ⚡ Edge Runtime Compatible - Works in Vercel Edge Runtime and serverless environments
- 🎯 TypeScript First - Fully typed with comprehensive TypeScript support
- 📱 SSR & Client-Side - Full support for server-side and client-side rendering
---
The middleware setup works for both App Router and Pages Router. Provider setup differs:
- App Router: Use app/layout.tsx with CsrfProvider.
- Pages Router: Use _app.tsx with CsrfProvider.
``bash`
npm install @csrf-armor/nextjsor
yarn add @csrf-armor/nextjsor
pnpm add @csrf-armor/nextjs
Add to your .env.local:
`bash`Generate with: openssl rand -base64 32
CSRF_SECRET=your-super-secret-csrf-key-min-32-chars-long
> ⚠️ Security Warning: Never use a default or weak secret in production!
Create middleware.ts in your project root:
`typescript
import {NextResponse} from 'next/server';
import type {NextRequest} from 'next/server';
import {createCsrfMiddleware} from '@csrf-armor/nextjs';
// Validate secret in production
if (process.env.NODE_ENV === 'production' && !process.env.CSRF_SECRET) {
throw new Error('CSRF_SECRET environment variable is required in production');
}
const csrfProtect = createCsrfMiddleware({
strategy: 'signed-double-submit',
secret: process.env.CSRF_SECRET!,
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax' // Use 'strict' for higher security if cross-origin not needed
}
});
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const result = await csrfProtect(request, response);
if (!result.success) {
// Security logging
console.warn('CSRF validation failed:', {
url: request.url,
method: request.method,
reason: result.reason,
ip: request.ip || 'unknown',
userAgent: request.headers.get('user-agent') || 'unknown',
});
return NextResponse.json(
{error: 'CSRF validation failed'},
{status: 403}
);
}
return result.response;
}
`
Wrap your app with the CSRF provider in app/layout.tsx (Next.js 13+ App Router):
`typescript jsx
// app/layout.tsx
import {CsrfProvider} from '@csrf-armor/nextjs/client';
import type {Metadata} from 'next';
export const metadata: Metadata = {
title: 'Your App',
description: 'Your app description',
};
export default function RootLayout({children}: {
children: React.ReactNode;
}) {
return (
$3
Wrap your app in
_app.tsx (Next.js 12+ Pages Router):`typescript jsx
// pages/_app.tsx
import {CsrfProvider} from '@csrf-armor/nextjs/client';export default function MyApp({Component, pageProps}) {
return (
);
}
`$3
`typescript jsx
'use client';
import {useCsrf} from '@csrf-armor/nextjs/client';
import {useState} from 'react';export function ContactForm() {
const {csrfToken, csrfFetch} = useCsrf();
const [message, setMessage] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
const formData = new FormData(e.currentTarget);
const response = await csrfFetch('/api/contact', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
}),
});
};
return (
);
}
`$3
`typescript
// app/api/your-route
import {NextRequest, NextResponse} from 'next/server';export async function POST(request: NextRequest) {
// CSRF validation happens automatically in middleware
}
`---
🔄 Routing System Setup
CSRF Armor supports both Next.js routing systems using the same root
middleware.ts file. See Quick Start.$3
`typescript
// middleware.ts (project root) - works for both routing systems
import {NextResponse} from 'next/server';
import type {NextRequest} from 'next/server';
import {createCsrfMiddleware} from '@csrf-armor/nextjs';const csrfProtect = createCsrfMiddleware({
strategy: 'signed-double-submit',
secret: process.env.CSRF_SECRET!,
});
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const result = await csrfProtect(request, response);
if (!result.success) {
return NextResponse.json(
{error: 'CSRF validation failed'},
{status: 403}
);
}
return result.response;
}
export const config = {
matcher: [
// Protect all routes except static files
'/((?!_next/static|_next/image|favicon.ico).*)',
],
};
`$3
`typescript jsx
// app/layout.tsx
import {CsrfProvider} from '@csrf-armor/nextjs/client';export default function RootLayout({children}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
`$3
`typescript jsx
// pages/_app.tsx
import type {AppProps} from 'next/app';
import {CsrfProvider} from '@csrf-armor/nextjs/client';export default function App({Component, pageProps}: AppProps) {
return (
);
}
`$3
The React hooks work identically in both App Router and Pages Router:
`typescript jsx
'use client'; // Only needed in App Routerimport {useCsrf} from '@csrf-armor/nextjs/client';
export function ContactForm() {
const {csrfToken, csrfFetch} = useCsrf();
const handleSubmit = async (e: React.FormEvent) => {
//...
try {
const response = await csrfFetch('/api/contact', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({message: 'Hello'}),
});
if (response.ok) {
console.log('Success!');
}
} catch (error) {
console.error('Error:', error);
}
};
return (
);
}
`---
🛡️ Security Strategies
Choose the strategy that best fits your security and performance requirements:
| Strategy | Security | Performance | Best For | Setup Complexity |
|----------------------------|----------|-------------|------------------|------------------|
| Signed Double Submit ⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | Most web apps | Medium |
| Double Submit | ⭐ | ⭐⭐⭐⭐⭐ | Local development | Easy |
| Signed Token | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | APIs, SPAs | Medium |
| Origin Check | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | Known origins | Easy |
| Hybrid | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | Maximum security | Hard |
$3
`typescript
const csrfProtect = createCsrfMiddleware({
strategy: 'signed-double-submit',
secret: process.env.CSRF_SECRET!,
});
`How it works:
- Client receives unsigned token in response header and accessible cookie
- Server stores signed token in httpOnly cookie
- Client submits unsigned token, server verifies against signed cookie
- Combines cryptographic protection with double-submit pattern
Best for: E-commerce, financial services, general web applications
$3
`typescript
const csrfProtect = createCsrfMiddleware({
strategy: 'double-submit',
});
`How it works:
- Same token stored in cookie and sent in header/form
- Relies on Same-Origin Policy for protection
Best for: Local development (Not recommended for production)
$3
`typescript
const csrfProtect = createCsrfMiddleware({
strategy: 'signed-token',
secret: process.env.CSRF_SECRET!,
token: {expiry: 3600}, // 1 hour
});
`How it works:
- HMAC-signed tokens with expiration timestamps
- Stateless validation using cryptographic signatures
Best for: APIs, SPAs, microservices
$3
`typescript
const csrfProtect = createCsrfMiddleware({
strategy: 'origin-check',
allowedOrigins: [
'https://yourdomain.com',
'https://www.yourdomain.com',
],
});
`How it works:
- Validates Origin/Referer headers against whitelist
- Lightweight validation with minimal overhead
Best for: Mobile app backends, known client origins
$3
`typescript
const csrfProtect = createCsrfMiddleware({
strategy: 'hybrid',
secret: process.env.CSRF_SECRET!,
allowedOrigins: ['https://yourdomain.com'],
});
`How it works:
- Combines signed token validation with origin checking
- Multiple layers of protection for maximum security
Best for: Banking, healthcare, enterprise applications
---
⚙️ Configuration
$3
`typescript
interface CsrfConfig {
strategy?: 'double-submit' | 'signed-double-submit' | 'signed-token' | 'origin-check' | 'hybrid';
secret?: string; // Required for signed strategies token?: {
expiry?: number; // Token expiry in seconds (default: 3600)
headerName?: string; // Header name (default: 'x-csrf-token')
fieldName?: string; // Form field name (default: 'csrf_token')
};
cookie?: {
name?: string; // Cookie name (default: 'csrf-token')
secure?: boolean; // Secure flag (default: true in production)
httpOnly?: boolean; // HttpOnly flag (default: false)
sameSite?: 'strict' | 'lax' | 'none'; // SameSite (default: 'lax')
path?: string; // Path (default: '/')
domain?: string; // Domain (optional)
maxAge?: number; // Max age in seconds (optional)
};
allowedOrigins?: string[]; // Allowed origins for origin-check
excludePaths?: string[]; // Paths to exclude from protection
skipContentTypes?: string[]; // Content types to skip
}
`$3
`typescript
// Development configuration
const developmentConfig = {
strategy: 'double-submit' as const,
cookie: {
secure: false, // Allow HTTP in development
sameSite: 'lax' as const
}
};// Production configuration
const productionConfig = {
strategy: 'signed-double-submit' as const,
secret: process.env.CSRF_SECRET!,
cookie: {
secure: true, // HTTPS only
sameSite: 'strict' as const,
domain: '.yourdomain.com'
}
};
const csrfProtect = createCsrfMiddleware(
process.env.NODE_ENV === 'production'
? productionConfig
: developmentConfig
);
`$3
`typescript
const csrfProtect = createCsrfMiddleware({
strategy: 'signed-double-submit',
secret: process.env.CSRF_SECRET!,
excludePaths: [
'/api/webhooks', // External webhooks
'/api/public', // Public API endpoints
'/health', // Health checks
'/api/auth/callback' // Auth callbacks
],
});
`---
🪝 React Hooks API
$3
The context provider that manages CSRF state across your application.
`typescript
interface CsrfProviderProps {
children: React.ReactNode;
config?: CsrfClientConfig;
}interface CsrfClientConfig {
cookieName?: string; // Cookie name to read token from (default: 'csrf-token')
headerName?: string; // Header name to send token in (default: 'x-csrf-token')
autoRefresh?: boolean; // Auto-refresh on focus/visibility (default: true)
}
`Features:
- ✅ Event-driven updates (no polling)
- ✅ Automatic token refresh from response headers
- ✅ Shared state across components
- ✅ Performance optimized with React.memo
Usage:
`typescript jsx
cookieName: 'my-csrf',
headerName: 'X-My-CSRF',
autoRefresh: true
}}>
`$3
Main hook for accessing CSRF functionality.
`typescript
const {csrfToken, csrfFetch, updateToken} = useCsrf();
`Returns:
-
csrfToken: string | null - Current CSRF token
- csrfFetch: (input, init?) => Promise - Fetch with automatic CSRF headers
- updateToken: () => void - Manually refresh token---
🔒 Security Best Practices
$3
`bash
Generate a strong secret
openssl rand -base64 32Or using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
``typescript
// Validate secret at startup
if (process.env.NODE_ENV === 'production') {
const secret = process.env.CSRF_SECRET;
if (!secret || secret.length < 32) {
throw new Error('CSRF_SECRET must be at least 32 characters in production');
}
}
`$3
`typescript
const csrfProtect = createCsrfMiddleware({
strategy: 'signed-double-submit',
secret: process.env.CSRF_SECRET!,
cookie: {
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'strict', // Strictest protection
httpOnly: false, // Required for client access
path: '/',
maxAge: 60 60 24, // 24 hours
// For subdomains:
// domain: '.yourdomain.com'
},
});
`$3
`typescript
// middleware.ts
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const result = await csrfProtect(request, response); if (result.success) {
// Add security headers
result.response.headers.set('X-Content-Type-Options', 'nosniff');
result.response.headers.set('X-Frame-Options', 'DENY');
result.response.headers.set('X-XSS-Protection', '1; mode=block');
result.response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
}
return result.response;
}
`---
🔧 Advanced Usage
$3
`typescript
// middleware.ts
import {createCsrfMiddleware} from '@csrf-armor/nextjs';const apiCsrf = createCsrfMiddleware({
strategy: 'signed-token',
secret: process.env.CSRF_SECRET!,
token: {expiry: 3600}
});
const webCsrf = createCsrfMiddleware({
strategy: 'signed-double-submit',
secret: process.env.CSRF_SECRET!,
});
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const {pathname} = request.nextUrl;
let result;
if (pathname.startsWith('/api/')) {
result = await apiCsrf(request, response);
} else {
result = await webCsrf(request, response);
}
return result.success ? result.response :
NextResponse.json({error: 'Forbidden'}, {status: 403});
}
``---
We welcome contributions! Areas where help is needed:
- Additional framework integrations
- Performance optimizations
- Security enhancements
- Documentation improvements
- Test coverage expansion
---
MIT © Muneeb Samuels
- @csrf-armor/core - Framework-agnostic CSRF protection
---
Questions? Open an issue
or start a discussion!