1.0.1 • Published 7 months ago

@csrf-armor/nextjs v1.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
7 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

7 months ago

1.2.1

7 months ago

1.2.0

7 months ago

1.1.1

7 months ago

1.1.0

7 months ago

1.0.1

7 months ago

1.0.0

7 months ago

0.0.2

7 months ago

0.0.1

7 months ago