CSRF Protection

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.

4 min read Level 3/5 #koa#csrf#security
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 mechanismCSRF risk?Why
Cookie sessionYesBrowser sends cookies automatically
JWT in Authorization headerNoBrowsers do not auto-attach custom headers
JWT in a cookieYesSame 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 →