Cookie delivery and CSRF¶
Set token_delivery="cookie" and FastAuth sets the access and refresh tokens as HttpOnly cookies instead of returning them in the JSON body.
config = FastAuthConfig(
...,
token_delivery="cookie",
cookie_samesite="lax", # default
cookie_httponly=True, # default
# cookie_secure defaults to (not debug) — True in prod, False in dev
)
Cookies set on login/register/refresh¶
api.cookies.set_auth_cookies sets three cookies on the response:
| Cookie | Default name | HttpOnly | Purpose |
|---|---|---|---|
| access token | access_token |
True |
Read by require_auth via cookie. max_age = jwt.access_token_ttl. |
| refresh token | refresh_token |
True |
Used by /auth/refresh when no body is sent. max_age = refresh_token_ttl (or remember_me_ttl with remember=true). |
| CSRF token | csrf_token |
False |
Readable by your JS, sent back as X-CSRF-Token. |
There is no /auth/cookies endpoint. The CSRF cookie is delivered as a side effect of any successful cookie-mode authentication response (register, login, refresh, OAuth callback, magic-link callback, passkey authenticate/complete).
CSRF protection¶
enforce_cookie_csrf runs when:
- The request was authenticated via the access cookie (not the Bearer header), and
- The method is POST, PUT, PATCH, or DELETE.
It checks csrf_token cookie == X-CSRF-Token header (constant-time compare_digest). Mismatch → 403 CSRF token missing or invalid.
Disable it only if you are exclusively using Bearer auth:
Client pattern (browser)¶
// After login, read the readable CSRF cookie:
const csrf = getCookie("csrf_token"); // document.cookie parsing
await fetch("/api/thing", {
method: "POST",
credentials: "include", // send access/refresh/csrf cookies
headers: { "X-CSRF-Token": csrf },
body: JSON.stringify(payload),
});
fetch from a same-origin browser automatically sends the cookies; for cross-origin, set credentials: "include" and cors_origins on the server.
Refresh flow with cookies¶
curl -X POST http://localhost:8000/auth/refresh \
-H "X-CSRF-Token: $CSRF" --cookie "refresh_token=...;csrf_token=..."
If the body is empty, /auth/refresh reads the refresh token from the cookie and runs CSRF. If the body contains {"refresh_token": "..."}, CSRF is skipped (a stolen refresh token is itself the secret — CSRF protection does not help here).
SameSite guidance¶
lax(default): top-level navigations work, cross-site POSTs do not. Good for most SPAs.strict: a freshly logged-in user following an external link back to your app appears unauthenticated until the link lands. Most defensive.none: requiresSecure=True(browsers rejectSameSite=Noneover HTTP). Use only when you legitimately need third-party embedded auth, and serve strictly over HTTPS.
Dev mode¶
debug=True sets cookie_secure=False automatically so you can test on http://localhost. Never enable debug in production — it also enables verbose errors.
Common mistakes¶
403 CSRF token missing or invalidwhen you think CSRF is off —csrf_enableddefaults toTrue. Set itFalseonly if you use Bearer exclusively.- Cookies not sent in tests — use
TestClientwithfollow_redirects=Falseand captureSet-Cookiefrom the login response; replay all three cookies on subsequent calls plus theX-CSRF-Tokenheader. Securecookies over plain HTTP in CI — setdebug=Truefor tests, or run under TLS.