hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Securing Your API with @hectoday/http

The Threat Landscape

  • What Could Go Wrong
  • Project Setup

Brute-Force Protection

  • Rate Limiting Login Attempts
  • Account Lockout
  • Timing Attack Prevention

CSRF Protection

  • What Is CSRF?
  • CSRF Tokens
  • CSRF for API Consumers

Token Hardening

  • Refresh Token Rotation
  • Token Revocation
  • Secure Token Storage

Password Reset

  • The Password Reset Flow
  • Building the Reset Routes
  • Reset Security

Putting It All Together

  • Security Headers
  • Logging and Monitoring
  • Security Checklist
  • Capstone: Hardened Auth API

CSRF for API Consumers

The problem with CSRF tokens and APIs

The double-submit cookie pattern works well when your frontend is a browser-based app that can read cookies and set headers. But what about other clients?

A mobile app does not have document.cookie. A third-party integration making server-to-server calls does not use cookies at all. Requiring a CSRF cookie-and-header pair from these clients adds friction for no security benefit — CSRF is a browser-specific attack.

Custom header verification

A simpler approach for API endpoints: require a custom header on every state-changing request. Any custom header will do:

// src/csrf.ts — add this alternative
export function requireCustomHeader(request: Request): true | Response {
  const header = request.headers.get("x-requested-with");
  if (!header) {
    return Response.json({ error: "Missing required header" }, { status: 403 });
  }
  return true;
}

Why does this work? A cross-site form submission (the classic CSRF vector) cannot set custom headers. HTML forms can only set the Content-Type header (and only to a few values). An attacker’s <form action="..." method="POST"> cannot include X-Requested-With.

A cross-site fetch request can set custom headers, but it triggers a CORS preflight (an OPTIONS request). If your server does not respond to the preflight with the right CORS headers allowing the attacker’s origin, the browser blocks the request.

So requiring any custom header is enough to block CSRF from cross-site forms, and CORS blocks cross-site fetch requests with custom headers.

When to use which approach

Double-submit cookie (previous lesson): Use when your browser frontend uses cookie-based sessions and you want explicit CSRF protection on top of SameSite=Lax. This is the belt-and-suspenders approach.

Custom header verification (this lesson): Use when your API serves a mix of browser and non-browser clients. The browser clients include the header from their JavaScript. Non-browser clients include it naturally (any HTTP client can set custom headers). No cookies involved.

Neither (token-based auth only): If all your clients use the Authorization: Bearer header and none use cookies, CSRF is not a concern. The browser does not attach the Authorization header automatically.

Choosing your strategy

For the app we are building (session-based auth for browsers, token-based auth for APIs), a practical approach is:

function csrfProtection(request: Request): true | Response {
  // Token-based auth: no CSRF needed
  if (request.headers.has("authorization")) {
    return true;
  }

  // Cookie-based auth: require CSRF protection
  return requireCsrf(request);
}

If the request includes an Authorization header, it is using token-based auth and CSRF protection is unnecessary. If it uses cookies (no Authorization header), require the CSRF token.

This lets both client types work without compromising security.

Use it in handlers

route.post("/users", {
  resolve: async (c) => {
    const csrf = csrfProtection(c.request);
    if (csrf instanceof Response) return csrf;

    // Now do the auth check (session or token)
    // ...
  },
});

Exercises

Exercise 1: Make a POST request with curl that includes X-Requested-With: XMLHttpRequest and verify it passes the custom header check. Then make the same request without the header and verify it fails with 403.

Exercise 2: Implement the combined csrfProtection function. Test it with a cookie-based session (should require CSRF token) and with a Bearer token (should pass without CSRF token).

Why can't an HTML form set custom headers like X-Requested-With?

← CSRF Tokens Refresh Token Rotation →

© 2026 hectoday. All rights reserved.