@csrf-armor/nextjs v1.2.1
@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.
Contents
- 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
✨ Features
- 🛡️ Multiple Security Strategies - Choose from 5 different CSRF protection methods
- 🔄 App Router & Pages Router - Full support for both Next.js routing systems
- 🪝 React Hooks -
useCsrfhook 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
- 🔄 Automatic Token Management - Smart token refresh and validation
🚀 Quick Start
The middleware setup works for both App Router and Pages Router. Provider setup differs:
- App Router: Use
app/layout.tsxwithCsrfProvider. - Pages Router: Use
_app.tsxwithCsrfProvider.
1. Installation
npm install @csrf-armor/nextjs
# or
yarn add @csrf-armor/nextjs
# or
pnpm add @csrf-armor/nextjs2. Environment Setup
Add to your .env.local:
# 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!
3. Create Middleware
Create middleware.ts in your project root:
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;
}4. Context Provider Setup (App Router)
Wrap your app with the CSRF provider in app/layout.tsx (Next.js 13+ App Router):
// app/layout.tsx
import {CsrfProvider} from '@csrf-armor/nextjs';
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 (
<html lang="en">
<body>
<CsrfProvider>{children}</CsrfProvider>
</body>
</html>
);
}4b. Context Provider Setup (Pages Router)
Wrap your app in _app.tsx (Next.js 12+ Pages Router):
// pages/_app.tsx
import {CsrfProvider} from '@csrf-armor/nextjs';
export default function MyApp({Component, pageProps}) {
return (
<CsrfProvider>
<Component {...pageProps} />
</CsrfProvider>
);
}5. Usage in Components
'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<HTMLFormElement>) => {
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 (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<textarea
name="message"
placeholder="Your Message"
required
rows={4}
className="w-full p-2 border rounded"
/>
</div>
<button
type="submit"
disabled={!csrfToken}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50">
{isSubmitting ? 'Sending...' : 'Send Message'}
</button>
</form>
);
}6. API Route Example
// 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.
Universal Middleware (Both App Router & Pages Router)
// 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).*)',
],
};App Router Provider Setup
// app/layout.tsx
import {CsrfProvider} from '@csrf-armor/nextjs';
export default function RootLayout({children}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<CsrfProvider>{children}</CsrfProvider>
</body>
</html>
);
}Pages Router Provider Setup
// pages/_app.tsx
import type {AppProps} from 'next/app';
import {CsrfProvider} from '@csrf-armor/nextjs';
export default function App({Component, pageProps}: AppProps) {
return (
<CsrfProvider>
<Component {...pageProps} />
</CsrfProvider>
);
}Using Hooks in Both Routing Systems
The React hooks work identically in both App Router and Pages Router:
'use client'; // Only needed in App Router
import {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 (
<form onSubmit={handleSubmit}>
<input name="message" placeholder="Your message" />
<button type="submit">Send</button>
</form>
);
}🛡️ 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 |
Signed Double Submit (Recommended)
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
Double Submit Cookie
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)
Signed Token
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
Origin Check
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
Hybrid Protection
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
Complete Configuration Reference
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
}Environment-Specific Configuration
// 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
);Path Exclusions
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
CsrfProvider
The context provider that manages CSRF state across your application.
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:
<CsrfProvider config={{
cookieName: 'my-csrf',
headerName: 'X-My-CSRF',
autoRefresh: true
}}>
<App/>
</CsrfProvider>useCsrf Hook
Main hook for accessing CSRF functionality.
const {csrfToken, csrfFetch, updateToken} = useCsrf();Returns:
csrfToken: string | null- Current CSRF tokencsrfFetch: (input, init?) => Promise<Response>- Fetch with automatic CSRF headersupdateToken: () => void- Manually refresh token
🔒 Security Best Practices
1. Strong Secret Management
# Generate a strong secret
openssl rand -base64 32
# Or using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"// 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');
}
}2. Cookie Security Configuration
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. Security Headers
// 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
Multiple CSRF Strategies
// 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});
}🤝 Contributing
We welcome contributions! Areas where help is needed:
- Additional framework integrations
- Performance optimizations
- Security enhancements
- Documentation improvements
- Test coverage expansion
📄 License
MIT © Muneeb Samuels
📦 Related Packages
- @csrf-armor/core - Framework-agnostic CSRF protection
Questions? Open an issue or start a discussion!