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
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:
-
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. -
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. -
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.
-
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-Tokenby 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
OPTIONSpreflight if you use custom headers. Ensure your server handlesOPTIONSrequests 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.exempton 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
randominstead ofsecretsoritsdangerous— 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
- Double-submit cookie pattern for stateless APIs — no server-side session storage needed, but requires careful cookie flags.
- SameSite cookie attribute (Strict/Lax) as a browser-enforced layer — easy to implement but not a complete solution on its own.
- 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
SameSitecookies 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.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.