Fix CSRF SameSite=Strict breaking login on iPad/Safari

Safari (iPadOS/iOS) blocks SameSite=Strict cookies on the initial
top-level navigation when it considers the request cross-site (links
from messengers, email, QR codes). The CSRF cookie was therefore never
set on first visit, and the subsequent login POST failed with 403
"CSRF failed".

Switch the CSRF cookie to SameSite=Lax — this is the OWASP recommended
default and matches industry practice. The auth (session) cookie keeps
SameSite=Strict, since it is only issued after a successful first-party
login POST and needs the stricter binding.
This commit is contained in:
2026-04-30 17:38:20 +00:00
parent be65be8fdb
commit cf68bc848f
+4 -4
View File
@@ -507,7 +507,7 @@ def issue_csrf_cookie(response: RedirectResponse) -> str:
value=token, value=token,
httponly=False, httponly=False,
secure=True, secure=True,
samesite="strict", samesite="lax",
max_age=COOKIE_MAX_AGE, max_age=COOKIE_MAX_AGE,
path="/", path="/",
) )
@@ -1846,7 +1846,7 @@ def index(request: Request, user: Optional[User] = Depends(get_current_user), db
"session_notice": session_notice, "session_notice": session_notice,
}, },
) )
response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="strict", path="/") response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="lax", path="/")
return response return response
services = db.scalars( services = db.scalars(
@@ -2073,7 +2073,7 @@ def login(
}, },
status_code=401, status_code=401,
) )
response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="strict", path="/") response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="lax", path="/")
return response return response
if not user_is_valid(user): if not user_is_valid(user):
csrf = request.cookies.get(CSRF_COOKIE) or secrets.token_urlsafe(24) csrf = request.cookies.get(CSRF_COOKIE) or secrets.token_urlsafe(24)
@@ -2086,7 +2086,7 @@ def login(
}, },
status_code=403, status_code=403,
) )
response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="strict", path="/") response.set_cookie(CSRF_COOKIE, csrf, httponly=False, secure=True, samesite="lax", path="/")
return response return response
response = RedirectResponse(url="/", status_code=303) response = RedirectResponse(url="/", status_code=303)