Stateless implementation of Cross-Site Request Forgery (XSRF) Protection by using Double Submit Cookie mitigation pattern
Project description
FastAPI CSRF Protect
Features
FastAPI extension that provides stateless Cross-Site Request Forgery (XSRF) Protection support.
Aimed to be easy to use and lightweight, we adopt Double Submit Cookie mitigation pattern.
If you were familiar with flask-wtf library this extension suitable for you.
This extension inspired by fastapi-jwt-auth 😀
- Storing
fastapi-csrf-tokenin cookies or serve it in template's context
Installation
The easiest way to start working with this extension with pip
pip install fastapi-csrf-protect
# or
uv add fastapi-csrf-protect
Getting Started
The following examples show you how to integrate this extension to a FastAPI App
Example Login Form
from fastapi import FastAPI, Request, Depends
from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi_csrf_protect import CsrfProtect
from fastapi_csrf_protect.exceptions import CsrfProtectError
from pydantic_settings import BaseSettings
app = FastAPI()
templates = Jinja2Templates(directory="templates")
class CsrfSettings(BaseSettings):
secret_key: str = "asecrettoeverybody"
cookie_samesite: str = "none"
@CsrfProtect.load_config
def get_csrf_config():
return CsrfSettings()
@app.get("/login")
def form(request: Request, csrf_protect: CsrfProtect = Depends()):
"""
Returns form template.
"""
csrf_token, signed_token = csrf_protect.generate_csrf_tokens()
response = templates.TemplateResponse(
"form.html", {"request": request, "csrf_token": csrf_token}
)
csrf_protect.set_csrf_cookie(signed_token, response)
return response
@app.post("/login", response_class=JSONResponse)
async def create_post(request: Request, csrf_protect: CsrfProtect = Depends()):
"""
Creates a new Post
"""
await csrf_protect.validate_csrf(request)
response: JSONResponse = JSONResponse(status_code=200, content={"detail": "OK"})
csrf_protect.unset_csrf_cookie(response) # prevent token reuse
return response
@app.exception_handler(CsrfProtectError)
def csrf_protect_exception_handler(request: Request, exc: CsrfProtectError):
return JSONResponse(status_code=exc.status_code, content={"detail": exc.message})
How to send the CSRF token in your client code
HTML Form (Server-side rendered)
<form method="post" action="/login">
<input type="hidden" name="token_key" value="{{ csrf_token }}">
<!-- other fields -->
</form>
AJAX (JavaScript)
fetch("/items/123", {
method: "DELETE",
headers: {
"X-CSRFToken": getCookie("csrftoken")
},
credentials: "include"
});
[!IMPORTANT]
- The flexible sub-package ignores the token_location setting — tokens from either header or body are always accepted.
- CSRF token validation still requires a matching CSRF cookie as in the base package.
- Priority is given to header over body when both are present.
📌 Flexible Mode (fastapi_csrf_protect.flexible)
Some applications combine Server-Side Rendering (SSR) with API endpoints in the same project. For example:
- SSR pages rendered with Jinja2 templates that use HTML forms (CSRF token in form body)
- AJAX / API calls (e.g. DELETE, PUT, PATCH) that pass the CSRF token in the HTTP header
The main fastapi-csrf-protect package is opinionated and expects the CSRF token in one location only (either header or body). For hybrid apps, this can be inconvenient.
The flexible sub-package provides a drop-in replacement for CsrfProtect that always accepts CSRF tokens from either the header or the form body, with the following priority:
- Header: X-CSRFToken
- Body: token_key (form-data)
When to use flexible
Use fastapi_csrf_protect.flexible if:
- You have both SSR pages and API endpoints in the same project.
- Some requests (like DELETE) cannot send a body but still require CSRF validation.
- You want to avoid maintaining two different CSRF configurations.
If your app only uses one method to send CSRF tokens, stick to the core package for a stricter policy.
Contributions
Prerequisites
- git - --fast-version-control
- python 3.9 and above - High-level general-purpose programming language
- uv - Extremely fast Python package & project manager, written in Rust
The following guide walks through setting up your local working environment using git
as distributed version control system and uv as Python package and version manager.
If you do not have git installed, run the following command.
Install using Homebrew (Darwin)
brew install git
Install via binary installer (Linux)
- Debian-based package management
sudo apt install git-all
- Fedora-based package management
sudo dnf install git-all
If you do not have uv installed, run the following command.
Install using Homebrew (Darwin)
brew install uv
Install using standalone installer (Darwin and Linux)
curl -LsSf https://astral.sh/uv/install.sh | sh
Once you have git distributed version control system installed, you can
clone the current repository and install any version of Python above version
3.9 for this project. The following commands help you set up and activate a
Python virtual environment where uv can download project dependencies from the PyPI
open-sourced registry defined under pyproject.toml file.
Set up environment and synchronize project dependencies
git clone git@github.com:aekasitt/fastapi-csrf-protect.git
cd fastapi-csrf-protect
uv venv --python 3.9.6
source .venv/bin/activate
uv sync --dev
Roadmap
- Fix CI/CD and GitHub Pages integration
- Add code samples when setting up and running tests
- Drop support for Python 3.9 by October, 2025
- (Syntax 3.9 -> 3.10) Replace
Optional[...]withNone | ... - (Syntax 3.9 -> 3.10) Replace
Union[..., ...]with... | ... - Correct front-end samples in
README.md - Add
flexibleexamples - Rewrite
Flexible Modesection inREADME.md - Experiment with
granian[uvloop]andgranian[rloop]
Getting started
To contribute to the project, fork the repository and clone to your local device and install preferred testing dependency pytest Alternatively, run the following command on your terminal to do so:
uv sync --dev
Testing can be done by the following command post-installation:
uv sync --dev --group tests
pytest
Change-logs
-
0.3.1 Adopt Double Submit Cookie
:construction: BREAKING CHANGE 0.3.0 -> 0.3.1:
generate_csrfmarked for deprecation -
0.3.2 Add
token_locationconfig (eitherbodyorheader); Unset to prevent token reuse:construction: BREAKING CHANGE 0.3.1 -> 0.3.2:
validate_csrfis now async -
0.3.5 Introduced Pydantic V2 related bug fixed in version 0.3.6; Affects
cookie_samesite -
0.3.6 Fixed
cookie_samesitevalidation bug introduced in previous version -
1.0.0 Remove deprecated
generate_csrf, please usegenerate_csrf_tokensreturning tuple -
1.0.1 Fix cookie unsetting when configuring lib with cookie
Secureand / orSameSite=None -
1.0.2 Improve boolean handling for
LoadConfig -
1.0.3 Failed experiement to integrate
mypyccompilation due to dependency injection pattern -
1.0.4 Added flexible mode when
token_locationis omitted and multiple location checks:construction: FAILED ROLLOUT 1.0.3 -> 1.0.4: Rolled out with WIP code; immediately deleted version from PyPI
-
1.0.5 Remove
@dataclassleftover from failed experiment; Clarify failure reasons under tests -
1.0.6 Fix
Stream consumedwhen submitted tokens via form data,isinstanceconsumes body -
1.0.7 Fix add preverification content submitted in
Flexible Mode; Add test selection flags
Run Examples
To run the provided examples, first you must install extra dependencies granian and minijinja Alternatively, run the following command on your terminal to do so
uv sync --group=examples
Running the example utilizing form submission
granian --interface asgi examples.body:app
Running the example utilizing headers via JavaScript
granian --interface asgi examples.header:app
License
This project is licensed under the terms of the MIT license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file fastapi_csrf_protect-1.0.7.tar.gz.
File metadata
- Download URL: fastapi_csrf_protect-1.0.7.tar.gz
- Upload date:
- Size: 207.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.20
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
888b15b232625aae5b997fbcf81ef45633a7694f0312a054f1eec6d132b295fb
|
|
| MD5 |
d6a8986ba1fb99499a5b036fef00bee4
|
|
| BLAKE2b-256 |
f61afedbcb4aba24ccc8abfb5d30e08112073c6a9f20b8d88adbdd3051ceedac
|
File details
Details for the file fastapi_csrf_protect-1.0.7-py3-none-any.whl.
File metadata
- Download URL: fastapi_csrf_protect-1.0.7-py3-none-any.whl
- Upload date:
- Size: 18.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.20
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca3c5b50564af932ac4ed3d06caeed61bf16eed13a31cfe2bdfc3f7c1e8612a3
|
|
| MD5 |
d94cb2cf774012b3b47f286badfff6ff
|
|
| BLAKE2b-256 |
bf10f248aab919678444723d557da918088e5c737b44e03e3aa4a0ad7afc7dae
|