CSRF Protection
Cross-Site Request Forgery (CSRF) protection prevents malicious websites from submitting forms on behalf of authenticated users. Guren provides built-in CSRF middleware that integrates seamlessly with sessions.
CSRF Protection
Cross-Site Request Forgery (CSRF) protection prevents malicious websites from submitting forms on behalf of authenticated users. Guren provides built-in CSRF middleware that integrates seamlessly with sessions.
The token spans two requests: it is issued on the GET that renders the form, and matched on the POST that submits it.
sequenceDiagram
participant B as Browser
participant M as CSRF middleware
participant C as Controller
B->>M: GET /form (safe method, not verified)
M->>M: issue the session token<br/>(a double-submit token for guests)
M->>C: run
C-->>B: form with a hidden _token / XSRF-TOKEN cookie
Note over B: A form on another site cannot read this token
B->>M: POST /form (with _token or X-XSRF-TOKEN)
M->>M: match the submitted token against the session value
alt they match
M->>C: run
C-->>B: 200
else missing or mismatched
M-->>B: 403
end
Setup
Enable CSRF protection by adding the middleware to your application:
// src/app.ts
import { createApp, createSessionMiddleware, createCsrfMiddleware } from '@guren/core'
const app = createApp()
// Optional — the token binds to a persisted session
app.use('*', createSessionMiddleware())
app.use('*', createCsrfMiddleware())
The middleware automatically:
- Generates a token per session, or a stateless double-submit token for guests
- Validates tokens on state-changing requests (POST, PUT, PATCH, DELETE)
- Allows safe methods (GET, HEAD, OPTIONS, QUERY) without validation — QUERY (RFC 10008) is safe by contract, so keep QUERY handlers read-only, or add
'QUERY'to themethodsoption to require tokens anyway
Including the Token in Forms
A native <form method="post"> must carry the token as a _token field, or Guren
rejects it with a 403. In an Inertia app, useForm() and <Link method="post">
send it for you — see Inertia.js Integration.
Use the csrfField() helper to generate a hidden input field:
// In your controller
import { Controller, getCsrfToken, csrfField } from '@guren/core'
import { pages } from '@/.guren/pages.gen'
export default class FormController extends Controller {
create() {
const token = getCsrfToken(this.ctx)
// Pass to your template/view
return this.inertia(pages.forms.Create, { csrfToken: token })
}
}
In your frontend form (React example):
function CreateForm({ csrfToken }: { csrfToken: string }) {
return (
<form method="POST" action="/posts">
<input type="hidden" name="_token" value={csrfToken} />
{/* form fields */}
<button type="submit">Create</button>
</form>
)
}
Or generate the hidden field directly:
const hiddenField = csrfField(ctx)
// Returns: <input type="hidden" name="_token" value="..." />
AJAX Requests
For JavaScript/AJAX requests, include the token in a header:
// The middleware sets a JavaScript-readable XSRF-TOKEN cookie
const csrfToken = decodeURIComponent(
document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/)?.[1] ?? '',
)
fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-XSRF-TOKEN': csrfToken,
},
body: JSON.stringify({ title: 'Hello' }),
})
Axios — and therefore Inertia.js — does this for you, so you only need the code
above for plain fetch.
The middleware accepts the token from three places, in this order:
- The
X-CSRF-TOKENheader - The
X-XSRF-TOKENheader, read from theXSRF-TOKENcookie - The
_tokenfield in an urlencoded, multipart, or JSON request body
These names are not configurable. If you turn the cookie off (cookie: false
below), pass the token to the page yourself with getCsrfToken(ctx) and send it
as X-CSRF-TOKEN — do this only for session-authenticated flows, because guest
tokens verify against the cookie and cannot work without it.
Configuration Options
createCsrfMiddleware({
// Routes to exclude from CSRF validation
exclude: ['/api/webhooks/*', '/api/public/*'],
// Custom error handler
onError: (ctx) => {
return ctx.json({ error: 'Invalid CSRF token' }, 403)
},
})
The remaining options rarely need changing:
| Option | Default | Purpose |
|---|---|---|
methods |
['POST', 'PUT', 'PATCH', 'DELETE'] |
Which HTTP methods require a token |
cookie |
true |
Issue the XSRF-TOKEN cookie on safe requests and successful mutations |
cookieOptions |
{ path: '/', sameSite: 'Lax' } |
Cookie attributes; secure is on when NODE_ENV is production, and in runtimes without process |
Excluding Routes
Some routes (like webhook endpoints) should skip CSRF validation:
createCsrfMiddleware({
exclude: [
'/api/webhooks/stripe',
'/api/webhooks/github',
'/api/public/*', // Wildcard patterns supported
],
})
Manual Token Verification
For custom validation logic, use verifyCsrfToken():
import { verifyCsrfToken, getCsrfToken } from '@guren/core'
import { Router } from '@guren/core'
export function registerWebRoutes(router: Router): void {
router.post('/custom', async (ctx) => {
const token = ctx.req.header('X-Custom-Token')
if (!verifyCsrfToken(ctx, token)) {
return ctx.json({ error: 'Invalid token' }, 403)
}
return ctx.json({ ok: true })
})
}
Token Regeneration
A session-bound token follows the session id, so it changes when:
- The session is first persisted (a brand-new session does not yet anchor a token)
session.regenerate()is called (recommended after login)
Guest tokens carry no session id and are reused until a session exists.
// After successful login
const session = getSessionFromContext(ctx)
await session.regenerate()
// New CSRF token is generated automatically
Security Best Practices
- Always use HTTPS - Tokens can be intercepted over HTTP
- Regenerate after login - Prevents session fixation attacks
- Don't expose tokens in URLs - Use POST bodies or headers
- Set secure cookie flags - The session middleware handles the session cookie; the
XSRF-TOKENcookie followscookieOptions
Inertia.js Integration
When using Inertia.js, CSRF is handled automatically through cookies. Ensure your Axios/fetch configuration includes credentials:
// resources/js/app.tsx
axios.defaults.withCredentials = true
Inertia automatically reads the XSRF-TOKEN cookie and includes it in requests.
Submit through Inertia, not a native form
That only covers requests Inertia sends through Axios. A native
<form method="post"> submits as a full browser navigation, which carries no
X-XSRF-TOKEN header — so Guren rejects it with a 403 unless the form itself
carries a _token hidden field.
In an Inertia page, prefer useForm():
import { useForm } from '@inertiajs/react'
function LogoutButton() {
const { post, processing } = useForm()
return (
<button type="button" onClick={() => post('/logout')} disabled={processing}>
Log out
</button>
)
}
<Link href="/logout" method="post" as="button"> works too, for a plain action
link. Reach for a native form only when you deliberately want a full page submit.