[Python] Why isn’t my cookie being set on a POST redirect in FastAPI with sqladmin custom...

Discussão em 'Python' iniciado por Stack, Março 17, 2025.

  1. Stack

    Stack Membro Participativo

    I’m building an admin panel with FastAPI using sqladmin, and I’ve implemented a custom authentication backend by extending sqladmin’s AuthenticationBackend. In my login method, when a POST request is made to /admin/login, I validate the credentials, create a JWT token with the claim is_admin=True, and then set this token as a cookie named "admin_token". The response is a RedirectResponse (HTTP 303) that redirects the user to the admin index page.

    However, while testing in Chrome and Safari, I found that the "admin_token" cookie is not being stored by the browser when returned from this POST redirect response. Interestingly, when I set the same cookie in a GET request (using a test endpoint), it gets stored correctly.

    Here’s the relevant code:

    admin_auth_service.py:


    import logging
    import jwt
    from fastapi import Request, status
    from sqladmin.authentication import AuthenticationBackend
    from starlette.responses import RedirectResponse, Response

    from app.core.config import settings
    from app.db.base import async_db_cntxman
    from app.schemas.admin_schema import AdminLoginRequest
    from app.services.admin_service import AdminService

    logger = logging.getLogger("app")

    class AdminAuthService(AuthenticationBackend):
    async def login(self, request: Request) -> bool | Response:
    if request.method != "POST":
    return False # GET on /admin/login shows the login form
    response = RedirectResponse(
    url=request.url_for("admin:index"), status_code=status.HTTP_303_SEE_OTHER
    )
    form_data = dict(await request.form())
    try:
    async with async_db_cntxman() as db:
    form_admin_req = AdminLoginRequest.model_validate(form_data)
    data = await AdminService.login(db, form_admin_req)
    response.set_cookie(
    key="admin_token",
    value=data.access_token,
    httponly=True, # Protects against XSS
    secure=False, # False for local development (HTTP)
    samesite="lax", # Helps mitigate CSRF issues
    max_age=3600, # 1 hour session duration
    path="/", # Valid for the entire admin interface
    domain="localhost", # Specify domain for the cookie
    )
    logger.info("Login successful.")
    return response
    except Exception as e:
    logger.error(f"Login error: {e!s}")
    return False

    async def logout(self, request: Request) -> bool | Response:
    response = RedirectResponse(
    url=request.url_for("admin:login"), status_code=status.HTTP_302_FOUND
    )
    response.delete_cookie("admin_token")
    return response

    async def authenticate(self, request: Request) -> Response | None:
    token = request.cookies.get("admin_token")
    if not token:
    return RedirectResponse(
    request.url_for("admin:login"), status_code=status.HTTP_302_FOUND
    )
    try:
    payload = jwt.decode(
    token,
    settings.SECRET_KEY,
    algorithms=[settings.ALGORITHM],
    )
    if not payload.get("is_admin"):
    return RedirectResponse(
    request.url_for("admin:login"), status_code=status.HTTP_302_FOUND
    )
    except jwt.ExpiredSignatureError:
    logger.warning("Admin token expired.")
    return RedirectResponse(
    request.url_for("admin:login"), status_code=status.HTTP_302_FOUND
    )
    except jwt.PyJWTError as e:
    logger.warning(f"JWT error in admin token: {e}")
    return RedirectResponse(
    request.url_for("admin:login"), status_code=status.HTTP_302_FOUND
    )
    async with async_db_cntxman() as db:
    admin = await AdminService.is_admin(db, payload.get("sub"))
    if not admin:
    return RedirectResponse(
    request.url_for("admin:login"), status_code=status.HTTP_302_FOUND
    )
    return None

    temp_router.py (Test endpoint that successfully sets a cookie):


    from fastapi import APIRouter, Response
    from app.utils.response import success_response

    router = APIRouter()

    @router.get("/setcookie")
    async def set_cookie(response: Response):
    response.set_cookie(key="test_cookie", value="cookie_value")
    response.set_cookie(
    key="admin_token",
    value="data.access_token",
    httponly=True,
    secure=False,
    samesite="lax",
    max_age=3600,
    path="/",
    domain="localhost",
    )
    response.headers["X-Debug"] = "Cookie-Test"
    return success_response("Cookie set successfully")



    The test endpoint (/setcookie) sets the "admin_token" cookie successfully, but in the login flow, the cookie isn’t stored, and subsequent requests redirect back to the login page due to missing authentication.

    My questions are: 1. Why isn’t the "admin_token" cookie being stored when it is set in the POST redirect response? 2. Is this issue caused by browser security policies related to setting cookies on redirect responses, or is there something in my code that I should adjust? 3. What are the best practices for setting authentication cookies in FastAPI to ensure they are reliably stored across redirects?

    Any help or insights would be greatly appreciated. Please let me know if you need further details.

    I tried several approaches to have the “admin_token” cookie persist after a POST login request that results in a redirect. Specifically, I: • Returned a RedirectResponse with status code 303 (See Other) and then with 302 (Found). • Set the cookie using different SameSite values: “None”, “Lax”, and “Strict”. • Tested setting the domain attribute (e.g., domain=“localhost”) and omitting it entirely.

    In every case—regardless of the combination of SameSite (None, Lax, Strict) and whether the domain was set or not—the cookie was not stored by the browser when the POST request triggered the redirect. I expected that the browser would accept and persist the cookie so that it would be included in subsequent requests, but it never happened either on Safari and Chrome.

    Continue reading...

Compartilhe esta Página