This guide shows you how to generate a CSRF token once in your root layout so that every page automatically receives the token, reducing duplication and ensuring consistency across your SvelteKit app.
Create (or update) your root layout server file (+layout.server.ts
) to check for an existing CSRF token in cookies. If not present, generate a new token using Node’s crypto module and set it in a cookie:
// +layout.server.ts
import { randomBytes } from 'crypto';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ cookies }) => {
let csrfToken = cookies.get('csrfToken');
if (!csrfToken) {
csrfToken = randomBytes(32).toString('hex');
// Not HttpOnly so that client-side code can access it for embedding in forms
cookies.set('csrfToken', csrfToken, { path: '/' });
}
return { csrfToken };
};
Explanation:
- We use
randomBytes
to generate a secure, random token. - The token is saved in a cookie (accessible by client-side code) so that it persists across requests.
- The token is returned as part of the layout load data, making it available on every page.
In your root layout file (+layout.svelte
), expose the token via the $page.data
store:
<!-- +layout.svelte -->
<script lang="ts">
export let data: { csrfToken: string };
</script>
<slot />
This file simply passes the data (including the CSRF token) to child components and pages.
Now, in any page or component that contains a form, include the CSRF token in a hidden field. Since the token is available via $page.data
, you can do something like:
<!-- ExampleForm.svelte -->
<script lang="ts">
import { page } from '$app/state';
</script>
<form method="POST">
<input type="hidden" name="csrfToken" value={page.data.csrfToken} />
<!-- other form fields -->
<button type="submit">Submit</button>
</form>
Explanation:
- The hidden field named
csrfToken
includes the token from the root layout, ensuring every form submission carries the token for validation.
In your form’s action (for example, in +page.server.ts
), compare the token submitted from the form with the one stored in the cookies:
// +page.server.ts
import { error } from '@sveltejs/kit';
import type { Actions } from './$types';
export const actions: Actions = {
default: async ({ request, cookies }) => {
const formData = await request.formData();
const tokenFromForm = formData.get('csrfToken');
const tokenFromCookie = cookies.get('csrfToken');
if (!tokenFromForm || tokenFromForm !== tokenFromCookie) {
console.error('CSRF token mismatch');
throw error(403, 'Invalid CSRF token');
}
// Process the form data here...
return { success: true };
}
};
Explanation:
- On form submission, the action compares the submitted token with the token in the cookie.
- If they don’t match, the request is rejected with a 403 error.
- Otherwise, the form data is processed as usual.
-
Token Rotation:
Consider rotating the token after a successful submission for extra security. -
Reusable CSRF Component:
If you have many forms, you might encapsulate the hidden input in a reusable component. -
Security:
Although this double-submit cookie pattern is widely used, always consider your application’s specific security needs when designing CSRF protection.