1.2.2 • Published 10 months ago

@csrf-armor/nextjs v1.2.2

Weekly downloads
-
License
MIT
Repository
github
Last release
10 months ago

@csrf-armor/nextjs

CI npm version License: MIT TypeScript Next.js

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

  • 🛡️ 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
  • 🔄 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.tsx with CsrfProvider.
  • Pages Router: Use _app.tsx with CsrfProvider.

1. Installation

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:

# 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:

StrategySecurityPerformanceBest ForSetup Complexity
Signed Double Submit⭐⭐⭐⭐⭐⭐⭐⭐⭐Most web appsMedium
Double Submit⭐⭐⭐⭐⭐Local developmentEasy
Signed Token⭐⭐⭐⭐⭐⭐⭐⭐APIs, SPAsMedium
Origin Check⭐⭐⭐⭐⭐⭐⭐⭐Known originsEasy
Hybrid⭐⭐⭐⭐⭐⭐⭐⭐Maximum securityHard

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 token
  • csrfFetch: (input, init?) => Promise<Response> - Fetch with automatic CSRF headers
  • updateToken: () => 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


Questions? Open an issue or start a discussion!

1.2.2

10 months ago

1.2.1

10 months ago

1.2.0

10 months ago

1.1.1

10 months ago

1.1.0

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago

0.0.2

10 months ago

0.0.1

10 months ago