FastAPI CSRF Protect
FastAPI CSRF Protect is a FastAPI extension providing stateless Cross-Site Request Forgery (XSRF) protection. It implements the Double Submit Cookie mitigation pattern, similar to `flask-wtf` and inspired by `fastapi-jwt-auth`. The library is designed to be lightweight and easy to use. The current version is 1.0.7, requiring Python 3.9 or newer, and it maintains an active release cadence.
Common errors
-
CsrfProtectError: CSRF token invalid
cause The CSRF token provided in the request (either in the header or form body) does not match the token in the signed cookie, or the cookie is missing/expired. This could also happen if `validate_csrf` was not called.fixEnsure the client-side code correctly sends the CSRF token (from `generate_csrf_tokens`) in the `X-CSRFToken` header or a form field named `csrf-token`. Verify the `CsrfSettings` (especially `secret_key` and `cookie_samesite`) are correctly configured and that `await csrf_protect.validate_csrf(request)` is present in your endpoint. -
AttributeError: 'CsrfProtect' object has no attribute 'secret_key'
cause The `CsrfSettings` class or the `@CsrfProtect.load_config` decorator was not properly defined or loaded before attempting to use `CsrfProtect`.fixMake sure you have a `BaseSettings` subclass (e.g., `CsrfSettings`) defining `secret_key` and decorated with `@CsrfProtect.load_config` in your application's startup phase. -
RuntimeError: `validate_csrf` must be awaited
cause You are calling `csrf_protect.validate_csrf(request)` without `await`. This is common after upgrading from versions prior to 0.3.2.fixChange the call to `await csrf_protect.validate_csrf(request)`. -
PydanticValidationError: 1 validation error for CsrfSettings.secret_key
cause The `secret_key` in your `CsrfSettings` is not being loaded correctly, potentially due to missing environment variable or an empty string being passed.fixEnsure that `CSRF_SECRET_KEY` environment variable is set in production, or provide a robust default value in your `CsrfSettings` definition, e.g., `secret_key: str = os.environ.get('CSRF_SECRET_KEY', 'a_strong_default_key')`.
Warnings
- breaking The `generate_csrf` function was deprecated in version 0.3.1. It was replaced by `generate_csrf_tokens` which returns both the plain and signed tokens.
- breaking The `validate_csrf` method became an `async` function.
- gotcha The library relies on explicit calls to `validate_csrf`. Simply injecting `CsrfProtect = Depends()` into an endpoint does NOT automatically secure it; you must explicitly call `await csrf_protect.validate_csrf(request)`.
- gotcha The main `fastapi-csrf-protect` package is opinionated and expects the CSRF token in either the header or the body, but not both simultaneously by default. For applications combining Server-Side Rendering (SSR) with API endpoints (hybrid apps), this can be inconvenient.
- gotcha When using cookie-based JWT authentication and sending CSRF tokens as headers, Swagger UI (and other API documentation tools) might fail to send the CSRF header correctly, breaking API documentation for protected endpoints.
Install
-
pip install fastapi-csrf-protect
Imports
- CsrfProtect
from fastapi_csrf_protect import CsrfProtect
- CsrfProtectError
from fastapi_csrf_protect.exceptions import CsrfProtectError
- CsrfProtect (flexible mode)
from fastapi_csrf_protect.flexible import CsrfProtect
Quickstart
import os
from fastapi import FastAPI, Request, Depends
from fastapi.responses import JSONResponse, HTMLResponse
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") # Ensure you have a 'templates' directory with 'form.html'
class CsrfSettings(BaseSettings):
secret_key: str = os.environ.get('CSRF_SECRET_KEY', 'a_super_secret_key_for_csrf_protection')
cookie_samesite: str = "lax"
@CsrfProtect.load_config
def get_csrf_config():
return CsrfSettings()
@app.exception_handler(CsrfProtectError)
async def csrf_protect_exception_handler(request: Request, exc: CsrfProtectError):
return JSONResponse(status_code=exc.status_code, content={'detail': exc.message})
@app.get("/login", response_class=HTMLResponse)
async def login_form(request: Request, csrf_protect: CsrfProtect = Depends()):
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 process_login(request: Request, csrf_protect: CsrfProtect = Depends()):
await csrf_protect.validate_csrf(request)
# Your login logic here
response: JSONResponse = JSONResponse(status_code=200, content={"detail": "Login successful"})
csrf_protect.unset_csrf_cookie(response) # Optional: prevent token reuse
return response
# To run this, you'll need a 'templates/form.html' file like:
# <form method="post" action="/login">
# <input type="hidden" name="csrf-token" value="{{ csrf_token }}">
# <label for="username">Username:</label>
# <input type="text" id="username" name="username"><br><br>
# <label for="password">Password:</label>
# <input type="password" id="password" name="password"><br><br>
# <input type="submit" value="Submit">
# </form>