CSRF Token Checks for APIs

Learn how to protect APIs with CSRF token checks. This secure development tutorial covers the core concept, step-by-step implementation, hands-on exercises, troubleshooting, and next steps.

Focus: protect apis with csrf token checks

Sponsored

You’ve built an API endpoint, and it works — but can you trust the requests hitting it? A curious link in an email, a crafted form on a malicious site, or a hidden image tag could trigger state-changing actions in your user’s session without them ever knowing. This is Cross-Site Request Forgery (CSRF), a silent threat that exploits the browser’s automatic inclusion of cookies. In this lesson, you’ll learn how to protect APIs with CSRF token checks, a battle-tested defense that ensures every state-changing request comes from your own frontend, not an attacker’s trap.

The problem this lesson solves

CSRF attacks succeed because browsers attach authentication cookies to every request — even those initiated by a third-party site. Imagine an online banking app: you’re logged in, and your session cookie is stored. You visit a forum that contains an image tag pointing to https://bank.example/transfer?amount=1000&to=attacker. Your browser fetches that URL, sends your cookie, and the bank processes the transfer. No interaction, no consent, just silent theft.

For APIs, the stakes are higher. APIs often live on separate subdomains or are consumed by single-page applications (SPAs). The same cookie-authentication mechanism applies, but with JSON endpoints, the attack surface grows — any request that mutates state, like changing a password or deleting a resource, becomes a target.

Why tokens? CSRF tokens break the attacker’s ability to forge requests because the token is a secret that changes per session or per request. The server validates it before processing, ensuring the request came from a context the user controls.

Without protection, your API is vulnerable to actions like:

  • Changing email or password
  • Placing unauthorized orders
  • Deleting user data
  • Modifying account settings

Core concept / mental model

Think of a CSRF token as a secret handshake between your frontend and your API. When a user loads your page, the server issues a unique, unpredictable token and embeds it in the page or returns it via a secure endpoint. For any state-changing request, the frontend includes that token — in a header, in the request body, or as a form field. The server verifies the token matches the one stored in the session. If it doesn’t, the request is rejected.

A mental model: you have a bank vault. The vault requires two keys — one is the session cookie (the user’s identity), and the other is the CSRF token (proof that the request came from the legitimate page). An attacker may have access to the first (via the user’s browser), but they can’t obtain the second because it’s tied to the user’s session and never sent cross-origin.

Key characteristics of an effective CSRF token:

  • Unpredictable: Generated with a cryptographically secure random generator
  • Tied to the session: Stored server-side and linked to the user’s session ID
  • Sent via a custom header or body: Preferred over cookies (which are auto-attached)
  • Compared server-side: The server must validate the token on every state-changing request

How it works step by step

Let’s walk through the token lifecycle:

  1. Token issuance: When a user logs in or loads the main page, the server generates a random token and stores it in the session. It then provides it to the client, typically in a response header (e.g., X-CSRF-Token) or in a JSON body.

  2. Token embedding: The client-side code stores the token (in memory, local storage, or a JavaScript variable) and includes it in subsequent requests — usually as a custom header like X-CSRF-Token.

  3. Token verification: On the server, for any state-changing request (POST, PUT, PATCH, DELETE), the framework compares the submitted token against the one stored in the session. If they match, the request proceeds; otherwise, the API returns a 403 Forbidden.

  4. Token rotation: After a successful request, you may choose to rotate the token (especially for login/logout actions) to reduce the window for replay attacks.

Pro tip: Use the double-submit cookie pattern if you need a stateless API. The server sends a random token as a cookie, and the client must echo it back in a header. The server checks that the two match, without storing the token server-side. This is common in microservices.

Hands-on walkthrough

Let’s implement CSRF protection in a Python API using Flask and the flask-wtf library, which provides CSRF protection out of the box.

First, install the required packages:

pip install flask flask-wtf

Now, create a Flask app with CSRF protection. We’ll generate a token and expose it via a /token endpoint, then verify it on a protected route.

from flask import Flask, session, jsonify, request
from flask_wtf.csrf import CSRFProtect, generate_csrf

app = Flask(__name__)
app.secret_key = 'supersecretkey'  # In production, use a strong secret from environment
csrf = CSRFProtect(app)

@app.route('/token', methods=['GET'])
def get_token():
    # Generate a CSRF token and return it to the client
    token = generate_csrf()
    return jsonify({'csrf_token': token})

@app.route('/transfer', methods=['POST'])
@csrf.exempt  # We'll handle CSRF manually to demonstrate
protected()  # This will be replaced by the manual check

Wait, that’s not right. Let’s write a clean version:

from flask import Flask, session, jsonify, request, abort
from flask_wtf.csrf import CSRFProtect, generate_csrf, validate_csrf
from itsdangerous import BadSignature

app = Flask(__name__)
app.secret_key = 'change-this-in-production'
csrf = CSRFProtect(app)

# Exempt the token endpoint from CSRF (it's GET only)
@app.route('/token', methods=['GET'])
@csrf.exempt
def get_token():
    token = generate_csrf()
    return jsonify({'csrf_token': token})

# Protected endpoint: requires CSRF token in X-CSRF-Token header
@app.route('/transfer', methods=['POST'])
def transfer():
    # CSRF protection is active by default for POST requests
    # The client must send the token in the 'X-CSRF-Token' header
    amount = request.json.get('amount')
    to = request.json.get('to')
    # Process the transfer...
    return jsonify({'status': 'success', 'amount': amount, 'to': to})

Now, the client must fetch the token and include it in requests. Here’s an example using fetch in JavaScript:

// Fetch the CSRF token
const tokenResponse = await fetch('/token');
const { csrf_token } = await tokenResponse.json();

// Send a state-changing request with the token
const response = await fetch('/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrf_token
  },
  body: JSON.stringify({ amount: 100, to: 'attacker' })
});

If the token is missing or invalid, the server returns a 400 Bad Request with a CSRF error message. Let’s test it:

curl -X POST http://localhost:5000/transfer \
  -H "Content-Type: application/json" \
  -d '{"amount":100, "to":"attacker"}'
# Returns: CSRF token missing or incorrect (400)

And with the token:

# First get token
TOKEN=$(curl -s http://localhost:5000/token | python -c "import sys,json; print(json.load(sys.stdin)['csrf_token'])")
# Then post
curl -X POST http://localhost:5000/transfer \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $TOKEN" \
  -d '{"amount":100, "to":"attacker"}'
# Returns: {"status":"success", ...}

Compare options / when to choose what

There are several approaches to CSRF protection. Which one is right depends on your architecture and needs.

Approach How it works Pros Cons Best for
Synchronizer token Server stores token in session, client echoes it in a header or form field Widely supported, strong security; works with traditional server-rendered apps Requires session state; a bit more complex in SPAs Classic web apps, server-side sessions
Double-submit cookie Server sends token as cookie; client must send it back as a header; server compares them Stateless, works with distributed systems Must ensure cookie is not readable by JavaScript (HttpOnly), but still vulnerable if XSS exists; token can be guessed if not random Microservices, stateless APIs
SameSite cookie attribute Set SameSite=Strict or Lax on session cookies; browser blocks cross-site cookie sending Simple, browser-enforced, no token needed Not all browsers fully supported (older ones); may break legitimate cross-site flows A baseline defense, easy to add
Custom header requirement Require a custom header like X-Requested-With on all state-changing requests Prevents simple form posts; no token management Not enough on its own; relies on browsers blocking cross-origin custom headers (true for most, but not all) Quick mitigation, but use in addition to tokens

For most APIs, synchronizer token with a custom header is the gold standard. For fully stateless microservices, double-submit cookie is a solid alternative. Always combine with SameSite attributes on cookies for defense-in-depth.

Troubleshooting & edge cases

1. CSRF token missing or incorrect

  • Cause: Client missed the header or sent the wrong token.
  • Fix: Ensure your frontend fetches the token and sends it in every state-changing request. Check that the header name matches (X-CSRF-Token by default in Flask-WTF).

2. Token expires during a long session

  • Cause: Server-side token timeout or session expiration.
  • Fix: Implement a token refresh mechanism. Generate a new token on each page load or periodically via a heartbeat endpoint.

3. Mobile apps or third-party clients

  • Cause: Non-browser clients don’t automatically send cookies; they may send custom headers but might not have a CSRF token.
  • Fix: For API-only clients, use an API key or OAuth token instead of CSRF tokens. CSRF tokens are only needed for browser-based requests.

4. Cross-origin requests (CORS)

  • Cause: Your API is on a different domain, and the token is not accessible due to CORS preflight.
  • Fix: When making cross-origin requests, the browser will send an OPTIONS preflight if you use custom headers. Ensure your server handles OPTIONS requests and doesn’t require CSRF on them. Set up CORS headers appropriately.

5. Double-submit cookie and XSS

  • Cause: If an attacker can inject JavaScript (XSS), they can read the token from the cookie or local storage.
  • Fix: Keep the cookie HttpOnly, but then you can’t read it from JavaScript for the double-submit pattern. Consider using a separate non-HttpOnly token cookie that is not sensitive, or rely on the synchronizer token pattern to keep the token in memory.

6. Logging/forms with false positives

  • Cause: Your form includes the token field, but the session is new or expired.
  • Fix: For login forms, exempt the login endpoint from CSRF (since there is no session yet), but enforce CSRF on all other state changes. In Flask-WTF, use @csrf.exempt on the login route.

What you learned & what's next

You now understand how to protect APIs with CSRF token checks. You learned that CSRF attacks can silently trigger unwanted actions by abusing the browser’s automatic cookie attachment, and that tokens provide a reliable guard by requiring a secret the attacker can’t forge. You saw a complete implementation with Flask and flask-wtf, compared different protection approaches, and know how to troubleshoot common issues.

Next in the Secure development track, you’ll dive into securing session management — how to handle session fixation, timeouts, and secure cookie flags. With CSRF protection in place, you’re building a solid foundation for robust API security.

Keep experimenting: add CSRF protection to a login form, test with and without the token, and observe the 400 errors. This hands-on practice will cement the concept.

Practice recap

Mini exercise: Enhance the Flask example by adding a login route that returns a new CSRF token. Then write a test that posts to /transfer without a token, and verify it returns a 400. Next, modify the double-submit cookie pattern — send a token as a cookie and require it in a header. This exercise will solidify your understanding of token validation flows.

Common mistakes

  • Relying solely on CORS policies — CORS does not prevent CSRF; an attacker can still submit a form or use an image tag that triggers a POST without reading the response.
  • Storing the CSRF token in a cookie without validation — in the double-submit pattern, if the token is in a readable cookie, an attacker could potentially guess or steal it via XSS.
  • Forgetting to exempt login endpoints — if you require a CSRF token on login, new users without a session will always fail; you must allow login to be CSRF-exempt (or issue a token before login).
  • Using a predictable token generator like random instead of secrets or itsdangerous — predictable tokens can be guessed by attackers.
  • Not rotating the token after a login/logout or after sensitive actions like password change — this can allow replay attacks.

Variations

  1. Double-submit cookie pattern for stateless APIs — no server-side session storage needed, but requires careful cookie flags.
  2. SameSite cookie attribute (Strict/Lax) as a browser-enforced layer — easy to implement but not a complete solution on its own.
  3. Custom header requirement without a token (e.g., X-Requested-With) — low effort but weak; use as a defense-in-depth addition.

Real-world use cases

  • A banking web app protecting transfer endpoints from cross-site forgeries by requiring a CSRF token header for every POST request.
  • A single-page application (SPA) with a separate API backend that uses synchronizer tokens to ensure only the legitimate frontend can change user settings.
  • A microservices architecture where each service validates a double-submit cookie token to ensure requests originate from trusted frontend clients.

Key takeaways

  • CSRF attacks exploit the browser's automatic cookie attachment; token checks verify that the request originated from a trusted context.
  • A secure CSRF token is unpredictable, tied to the session, and sent via a custom header or body — not as a cookie read by JavaScript.
  • The synchronizer token pattern is the most robust for traditional server-rendered apps; double-submit cookies suit stateless microservices.
  • Use SameSite cookies as a baseline, but never rely on them alone — combine with token validation for true protection.
  • Always exclude login endpoints from CSRF checks (or issue a pre-login token) and rotate tokens after sensitive actions.
  • Troubleshoot common issues like missing tokens, CORS preflight, and XSS risks to maintain a solid defense.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.