Skip to content

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:

  1. The request was authenticated via the access cookie (not the Bearer header), and
  2. 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:

config = FastAuthConfig(..., csrf_enabled=False)

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: requires Secure=True (browsers reject SameSite=None over 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 invalid when you think CSRF is off — csrf_enabled defaults to True. Set it False only if you use Bearer exclusively.
  • Cookies not sent in tests — use TestClient with follow_redirects=False and capture Set-Cookie from the login response; replay all three cookies on subsequent calls plus the X-CSRF-Token header.
  • Secure cookies over plain HTTP in CI — set debug=True for tests, or run under TLS.