SameSite Cookies and koa-csrf Tokens for Session Apps
CSRF Protection
Cookie-based sessions are vulnerable to Cross-Site Request Forgery. Learn how SameSite flags and synchronizer tokens (koa-csrf) work together to protect state-mutating routes.
What you'll learn
- Explain why session apps need CSRF protection but JWT-in-header APIs do not
- Configure SameSite cookies as the first layer of defence
- Issue and validate CSRF tokens with koa-csrf
CSRF (Cross-Site Request Forgery) tricks a logged-in user’s browser into sending an authenticated request to your server from a malicious site. The browser automatically attaches cookies — including your session cookie — to same-domain requests, so the server cannot tell the request is forged.
When You Need CSRF Protection
| Auth mechanism | CSRF risk? | Why |
|---|---|---|
| Cookie session | Yes | Browser sends cookies automatically |
JWT in Authorization header | No | Browsers do not auto-attach custom headers |
| JWT in a cookie | Yes | Same as session cookie |
If you use koa-session or store a JWT in a cookie, you need CSRF protection.
REST APIs that accept tokens only in the Authorization header are safe.
Layer 1: SameSite Cookies
Set sameSite: "lax" (or "strict") on every auth cookie. This stops CSRF
for cross-site top-level navigation (the most common attack vector) with zero
application code.
app.use(
session(
{
key: "koa.sess",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax", // blocks most CSRF
signed: true,
},
app
)
); sameSite: "strict" blocks all cross-site cookies but breaks OAuth callback
redirects. "lax" is the pragmatic default.
Layer 2: Synchronizer Token with koa-csrf
SameSite alone does not protect against same-site subdomains or older
browsers. Add a synchronizer token for defense-in-depth.
npm install koa-csrf import CSRF from "koa-csrf";
// koa-csrf requires koa-session (mounted before this).
app.use(
new CSRF({
invalidSessionSecretMessage: "Invalid session",
invalidSessionSecretStatusCode: 403,
invalidTokenMessage: "Invalid CSRF token",
invalidTokenStatusCode: 403,
excludedMethods: ["GET", "HEAD", "OPTIONS"],
disableQuery: false,
})
); Sending the Token to the Client
Expose the token on a GET endpoint or embed it in your HTML template. The client must echo it back on every state-mutating request.
router.get("/csrf-token", async (ctx) => {
ctx.body = { csrfToken: ctx.csrf };
}); On the client, send the token in the request body, query string, or the
X-CSRF-Token header:
// Fetch example (SPA)
const { csrfToken } = await fetch("/csrf-token").then((r) => r.json());
await fetch("/transfer", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrfToken,
},
body: JSON.stringify({ amount: 100 }),
}); Up Next
Protect auth endpoints from brute-force and credential-stuffing attacks with Redis-backed rate limiting.
Rate Limiting →