Skip to content

CSRF Protection

ideal-auth does not handle CSRF protection. This is by design — CSRF mitigation is a framework-level concern, and every major framework has its own mechanism. This guide explains the threat and walks through prevention strategies for each supported framework.


Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks a user’s browser into making an authenticated request to your application. Because the browser automatically attaches cookies (including session cookies) to every request to your domain, the attacker can perform actions as the logged-in user without their knowledge.

Example attack:

  1. A user is logged into your application at app.example.com
  2. The user visits evil.example.com, which contains a hidden form:
    <form action="https://app.example.com/api/transfer" method="POST">
    <input type="hidden" name="to" value="attacker" />
    <input type="hidden" name="amount" value="1000" />
    </form>
    <script>document.forms[0].submit();</script>
  3. The browser submits the form with the user’s session cookie attached
  4. Your server processes the transfer as if the user initiated it

This is why CSRF protection matters for any application that uses cookie-based authentication — which includes ideal-auth.


CSRF protection requires intercepting incoming requests before they reach your application logic. This is inherently framework-specific:

  • Next.js Server Actions have built-in protection
  • SvelteKit form actions have built-in protection
  • Express requires middleware
  • Hono has a built-in middleware

Implementing CSRF at the auth library level would mean coupling to a specific framework’s request/response model, which contradicts ideal-auth’s framework-agnostic design. Instead, ideal-auth provides the SameSite cookie attribute (default: lax) as a foundational defense layer, and you add framework-specific CSRF protection on top.


Server Actions have built-in CSRF protection. Next.js automatically validates the Origin header on all Server Action requests and rejects cross-origin submissions. No configuration is needed.

API Route Handlers (app/api/*/route.ts) do not have automatic CSRF protection. If you accept state-changing requests via API routes, validate the Origin header manually:

app/api/example/route.ts
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const headerStore = await headers();
const origin = headerStore.get('origin');
const host = headerStore.get('host');
if (!origin || new URL(origin).host !== host) {
return NextResponse.json(
{ error: 'Invalid origin' },
{ status: 403 },
);
}
// ... handle the request
}

If your framework does not provide built-in CSRF protection, the double-submit cookie pattern is a reliable framework-agnostic fallback. Here is how it works:

  1. On page load, the server sets a random CSRF token in a cookie (sameSite: 'strict', not httpOnly — JavaScript needs to read it)
  2. On form submission, JavaScript reads the token from the cookie and includes it as a request header (e.g., X-CSRF-Token)
  3. On the server, the middleware compares the token in the header to the token in the cookie. If they match, the request is legitimate

This works because:

  • A cross-origin attacker can cause the browser to send cookies, but cannot read cookies belonging to your domain
  • The attacker cannot set the X-CSRF-Token header because they cannot read the cookie value
  • SameSite: strict on the CSRF cookie prevents the cookie from being sent in cross-origin requests at all
Example: manual double-submit implementation
import { randomBytes } from 'node:crypto';
// Generate and set the CSRF token cookie
function setCsrfToken(setCookie: (name: string, value: string, options: any) => void): string {
const token = randomBytes(32).toString('hex');
setCookie('csrf_token', token, {
httpOnly: false, // JavaScript must read this
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production',
path: '/',
});
return token;
}
// Validate the CSRF token on incoming requests
function validateCsrfToken(
cookieToken: string | undefined,
headerToken: string | undefined,
): boolean {
if (!cookieToken || !headerToken) return false;
// Use timing-safe comparison
return cookieToken.length === headerToken.length &&
require('node:crypto').timingSafeEqual(
Buffer.from(cookieToken),
Buffer.from(headerToken),
);
}

ideal-auth defaults to sameSite: 'lax' on session cookies. This provides baseline CSRF protection by itself:

SameSite valueBehavior
strictCookie is never sent in cross-origin requests. Maximum protection, but breaks legitimate navigation (e.g., clicking a link from an email)
lax (default)Cookie is sent for top-level navigations (GET) but not for cross-origin form submissions (POST). Protects against the most common CSRF vectors
noneCookie is always sent. Requires secure: true. No CSRF protection from this attribute

SameSite: lax blocks the classic CSRF attack (cross-origin POST) while still allowing users to follow links to your app without losing their session. For most applications, this is the correct default.


FrameworkForms/ActionsAPI Routes
Next.jsBuilt-in (Server Actions)Manual Origin check
SvelteKitBuilt-in (form actions)Manual Origin check
Nuxtnuxt-csrf module or manualnuxt-csrf module or manual
TanStack StartManual Origin checkManual Origin check
Expresscsrf-csrf or double-submitcsrf-csrf or double-submit
HonoBuilt-in csrf() middlewareBuilt-in csrf() middleware
ElysiaManual Origin checkManual Origin check