UNPKG

@csrf-armor/nextjs

Version:

CSRF protection middleware for Next.js applications

682 lines (526 loc) 18.7 kB
# @csrf-armor/nextjs <img src="https://cdn.nebz.dev/csrf-armor/logo.jpeg" alt="CSRF Armor" /> [![CI](https://github.com/muneebs/csrf-armor/workflows/CI/badge.svg)](https://github.com/muneebs/csrf-armor/actions/workflows/ci.yml) [![npm version](https://badge.fury.io/js/@csrf-armor%2Fnextjs.svg)](https://badge.fury.io/js/@csrf-armor%2Fnextjs) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/) [![Next.js](https://img.shields.io/badge/Next.js-13%2B-black.svg)](https://nextjs.org/) **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](#-features) - [Quick Start](#-quick-start) - [Routing System Setup](#-routing-system-setup) - [Context Provider Setup (App Router)](#4-context-provider-setup-app-router) - [Context Provider Setup (Pages Router)](#4b-context-provider-setup-pages-router) - [Usage in Components](#5-usage-in-components) - [API Route Example](#6-api-route-example) - [Security Best Practices](#-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** - `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 --- ## 🚀 Quick Start 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`. ### 1. Installation ```bash npm install @csrf-armor/nextjs # or yarn add @csrf-armor/nextjs # or pnpm add @csrf-armor/nextjs ``` ### 2. Environment Setup 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! ### 3. Create Middleware 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; } ``` ### 4. Context Provider Setup (App Router) 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 ( <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): ```typescript jsx // pages/_app.tsx import {CsrfProvider} from '@csrf-armor/nextjs/client'; export default function MyApp({Component, pageProps}) { return ( <CsrfProvider> <Component {...pageProps} /> </CsrfProvider> ); } ``` ### 5. Usage in Components ```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<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 ```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](#-quick-start). ### Universal Middleware (Both App Router & Pages Router) ```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).*)', ], }; ``` ### App Router Provider Setup ```typescript jsx // app/layout.tsx import {CsrfProvider} from '@csrf-armor/nextjs/client'; export default function RootLayout({children}: { children: React.ReactNode; }) { return ( <html lang="en"> <body> <CsrfProvider>{children}</CsrfProvider> </body> </html> ); } ``` ### Pages Router Provider Setup ```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 ( <CsrfProvider> <Component {...pageProps} /> </CsrfProvider> ); } ``` ### Using Hooks in Both Routing Systems The React hooks work identically in both App Router and Pages Router: ```typescript jsx '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) ```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 ### Double Submit Cookie ```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) ### Signed Token ```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 ### Origin Check ```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 ### Hybrid Protection ```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 ### Complete Configuration Reference ```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 } ``` ### Environment-Specific Configuration ```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 ); ``` ### Path Exclusions ```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 ### CsrfProvider 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 <CsrfProvider config={{ cookieName: 'my-csrf', headerName: 'X-My-CSRF', autoRefresh: true }}> <App/> </CsrfProvider> ``` ### useCsrf Hook Main hook for accessing CSRF functionality. ```typescript const {csrfToken, csrfFetch, updateToken} = useCsrf(); ``` **Returns:** - `csrfToken: string | null` - Current CSRF token - `csrfFetch: (input, init?) => Promise<Response>` - Fetch with automatic CSRF headers - `updateToken: () => void` - Manually refresh token --- ## 🔒 Security Best Practices ### 1. Strong Secret Management ```bash # Generate a strong secret openssl rand -base64 32 # Or 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'); } } ``` ### 2. Cookie Security Configuration ```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. Security Headers ```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 ### Multiple CSRF Strategies ```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}); } ``` --- ## 🤝 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](https://github.com/muneebs) ## 📦 Related Packages - **[@csrf-armor/core](../core)** - Framework-agnostic CSRF protection --- **Questions?** [Open an issue](https://github.com/muneebs/csrf-armor/issues) or [start a discussion](https://github.com/muneebs/csrf-armor/discussions)!