Skip to content

Mixed-Auth CORS & CSRF

Your API serves two types of clients:

  • Browser clients — use CSRF cookies for request integrity, need CSRF protection, and make cross-origin requests subject to CORS policy
  • Mobile or service clients — use Bearer tokens in the Authorization header, are not vulnerable to CSRF, and do not send cookies

You need to configure CORS and CSRF so that browser routes get cross-origin cookie support with CSRF protection, while API-only routes skip CSRF entirely.

CORS is configured at the transport level — it applies to every request before the route pipeline runs. For mixed-auth APIs, enable credentials and explicitly list X-CSRF-TOKEN in the allowed headers so browsers can send the CSRF token on cross-origin requests.

import { compose, cors } from "@centralping/ergo";
const pipeline = compose(
cors({
origins: ["https://app.example.com"],
allowCredentials: true,
allowHeaders: [
"Content-Type",
"Authorization",
"X-CSRF-TOKEN",
],
}),
);

CSRF runs in the pipeline at Stage 2 (Authorization) — it can be enabled or disabled per route. Set csrf options on routes that use cookie-based auth, and csrf: false on routes that use Bearer tokens.

import { compose, cookie, csrf, authorization } from "@centralping/ergo";
const csrfMiddleware = csrf({ secret: process.env.CSRF_SECRET });
// Browser route — CSRF protection + Bearer auth
const browserPipeline = compose(
{fn: cookie(), setPath: "cookies"},
{fn: csrfMiddleware.verify, setPath: "csrf"},
{fn: authorization({
strategies: [{
type: "Bearer",
authorizer: async (attributes, token) => {
const session = await verifySessionToken(token);
return session
? { authorized: true, info: { user: session.user } }
: { authorized: false };
},
}],
}), setPath: "auth"},
);
// API route — Bearer auth, no CSRF
const apiPipeline = compose(
{fn: authorization({
strategies: [{
type: "Bearer",
authorizer: async (attributes, token) => {
const user = await verifyJwt(token);
return user
? { authorized: true, info: { user } }
: { authorized: false };
},
}],
}), setPath: "auth"},
);

A complete router with transport CORS, default CSRF, and per-route overrides:

import {
compose,
cookie,
cors,
csrf,
authorization,
} from "@centralping/ergo";
const csrfMiddleware = csrf({ secret: process.env.CSRF_SECRET });
const bearerStrategy = {
type: "Bearer",
authorizer: async (attributes, token) => {
const user = await verifyJwt(token);
return user
? { authorized: true, info: { user } }
: { authorized: false };
},
};
const corsMiddleware = cors({
origins: ["https://app.example.com"],
allowCredentials: true,
allowHeaders: [
"Content-Type",
"Authorization",
"X-CSRF-TOKEN",
],
});
// Browser route: CORS + cookies + CSRF + auth
const settingsPipeline = compose(
corsMiddleware,
{fn: cookie(), setPath: "cookies"},
{fn: csrfMiddleware.verify, setPath: "csrf"},
{fn: authorization({ strategies: [bearerStrategy] }), setPath: "auth"},
(req, res, acc) => ({
response: { body: { userId: acc.auth.user.id } },
}),
);
// API route: CORS + auth (no cookies, no CSRF)
const webhookPipeline = compose(
corsMiddleware,
{fn: authorization({ strategies: [bearerStrategy] }), setPath: "auth"},
(req, res, acc) => ({
response: { body: { received: true } },
}),
);

Why CORS Is Transport-Level and CSRF Is Per-Route

Section titled “Why CORS Is Transport-Level and CSRF Is Per-Route”

CORS and CSRF operate at different layers of the request lifecycle:

  • CORS validates the origin of the request before any route logic runs. It operates on HTTP transport semantics (preflight OPTIONS handling, response headers) and applies uniformly to all routes. In ergo-router, CORS is configured in transport because it runs before route matching.

  • CSRF validates request integrity — it ensures a browser request was intentionally initiated by the user, not by a malicious cross-site script. It operates at the pipeline level (Stage 2: Authorization) because different routes may need different CSRF policies depending on their auth mechanism.

Auth mechanismClient typeCSRF needed?Rationale
Session cookieBrowserYesBrowsers auto-attach cookies on every request — CSRF prevents forged submissions
Bearer tokenMobile/serviceNoTokens are explicitly attached by the client — not auto-sent by the browser
Cookie + Bearer (mixed)BothPer-routeEnable CSRF on cookie-auth routes, disable on Bearer-only routes
None (public)AnyNoNo credentials to protect

sameSite: 'Strict' and Cross-Site Requests

Section titled “sameSite: 'Strict' and Cross-Site Requests”

ergo’s CSRF middleware locks both cookies to sameSite: 'Strict'. This means CSRF cookies are sent on same-site requests and withheld on cross-site requests. The SameSite attribute operates on the site boundary (registrable domain), not the origin boundary — so app.example.com and api.example.com are cross-origin but same-site (both under example.com).

This has two implications:

  1. Cross-origin, same-site SPAs (e.g., app.example.com calling api.example.com) — SameSite=Strict cookies are delivered because the request is same-site. However, the CSRF cookies are host-only by default (scoped to the host that set them). If your API at api.example.com sets CSRF cookies, they are sent back to api.example.com on same-site requests — no additional domain configuration is needed for this topology.

  2. Cross-site integrations (e.g., other-domain.com calling your API) — the browser will not send SameSite=Strict cookies on cross-site requests. These clients should use Bearer token authentication with csrf: false.

When a route has csrf: false, the CSRF middleware is removed from the pipeline entirely — it does not run. A Bearer client that happens to have stale CSRF cookies from a previous browser session is unaffected because:

  • On csrf: false routes, the cookie is ignored — no verification occurs
  • On CSRF-enabled routes, a Bearer client without the X-CSRF-TOKEN header would fail CSRF verification with 403 — this is correct behavior, since that route expects cookie-based auth

The resolve() function in ergo-router handles each config key independently: csrf: false disables CSRF regardless of the authorization setting, and vice versa.