Tutorial: Add CSRF Protection to Forms
In this tutorial you will add CSRF protection to a Remix application with forms, generate tokens for both HTML forms and JavaScript API requests, and handle validation errors.
Prerequisites
- A Remix project with session middleware configured (see the session-middleware docs).
Step 1: Add the CSRF Middleware
The CSRF middleware depends on sessions to store the token. Add it after the session middleware:
import { createRouter } from 'remix/fetch-router'
import { session } from 'remix/session-middleware'
import { csrf, getCsrfToken } from 'remix/csrf-middleware'
import { formData } from 'remix/form-data-middleware'
import { route } from 'remix/fetch-router/routes'
let routes = route({
newPost: '/posts/new',
createPost: '/posts',
})
let router = createRouter({
middleware: [
session(sessionCookie, sessionStorage),
csrf(),
formData(),
],
})The middleware order matters:
session()-- makes the session available.csrf()-- generates and validates tokens using the session.formData()-- parses form bodies so CSRF can read the token field.
Step 2: Embed the Token in Forms
Use getCsrfToken() to retrieve the token and include it as a hidden field:
import { getCsrfToken } from 'remix/csrf-middleware'
router.get(routes.newPost, ({ context }) => {
let token = getCsrfToken(context)
return html`
<form method="post" action="/posts">
<input type="hidden" name="_csrf" value="${token}" />
<label for="title">Title</label>
<input type="text" id="title" name="title" required />
<label for="body">Body</label>
<textarea id="body" name="body" required></textarea>
<button type="submit">Create Post</button>
</form>
`
})The hidden <input> with name="_csrf" is what the middleware checks on submission.
Step 3: Handle Form Submissions
Your POST handler does not need any CSRF-specific code. The middleware validates the token before your handler runs:
import { FormData } from 'remix/form-data-middleware'
router.post(routes.createPost, async ({ context }) => {
// If we reach here, the CSRF token is valid
let data = context.get(FormData)
let title = data.get('title')
let body = data.get('body')
await db.create(posts, { title, body })
return redirect('/posts')
})If the token is missing or invalid, the middleware returns a 403 Forbidden response and your handler never executes.
Step 4: Protect JavaScript API Requests
For requests made with fetch() from the client, you can send the token in a header instead of a form field. First, expose the token in your page's HTML:
router.get(routes.app, ({ context }) => {
let token = getCsrfToken(context)
return html`
<html>
<head>
<meta name="csrf-token" content="${token}" />
</head>
<body>
<div id="app"></div>
<script src="/app.js"></script>
</body>
</html>
`
})Then read it from the client and include it in fetch requests:
// client-side JavaScript
let csrfToken = document.querySelector('meta[name="csrf-token"]').content
async function createPost(title, body) {
let response = await fetch('/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken,
},
body: JSON.stringify({ title, body }),
})
return response.json()
}The middleware checks both the _csrf form field and the X-CSRF-Token header. Either one is sufficient.
Step 5: Customize Field and Header Names
If your application uses different conventions (e.g., Rails-style authenticity_token):
csrf({
fieldName: 'authenticity_token',
headerName: 'X-Authenticity-Token',
})Update your forms and client code to match:
<input type="hidden" name="authenticity_token" value="${token}" />Step 6: Test CSRF Protection
Verify that requests without a valid token are rejected:
# This should return 403 Forbidden (no token)
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "title=Hello&body=World"
# This should also return 403 (wrong token)
curl -X POST http://localhost:3000/posts \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "title=Hello&body=World&_csrf=invalid-token"Both requests should fail with 403. Only requests with a valid token from a real session succeed.
What You Learned
- How to add CSRF middleware after the session middleware.
- How to embed CSRF tokens in HTML forms.
- How to send CSRF tokens from JavaScript via headers.
- How to customize field and header names.
- How to verify CSRF protection is working.
Next Steps
- API Reference -- Full details on options and behavior.
- cop-middleware -- Tokenless cross-origin protection as an alternative or complement.
- form-data-middleware -- Parsing the form data that CSRF validates.