katospiegel's picture
fix(auth): SameSite=None; Secure login cookie for HF iframe
3e5ba5d verified
Raw
History Blame Contribute Delete
2.33 kB
"""Password login routes.
The frontend POSTs ``{password}`` to ``/api/auth/login``; if the value
matches ``APP_PASSWORD`` (env), an HMAC token is set as an httpOnly cookie
and all subsequent ``/api/*`` requests authenticate via that cookie.
When ``APP_PASSWORD`` is unset, login still works (any password accepted)
and the cookie is not strictly required — useful for local dev or when the
deployment sits behind a Cloudflare tunnel without per-user auth.
"""
from __future__ import annotations
import os
from fastapi import APIRouter, HTTPException, Response, status
from ai_agent.api.deps import (
AUTH_COOKIE_NAME,
auth_disabled,
make_cookie_value,
verify_password,
)
from ai_agent.api.schemas import LoginRequest, LoginResponse
router = APIRouter(prefix="/api/auth", tags=["auth"])
@router.get("/status")
def status_route() -> dict:
"""Tells the frontend whether auth is enforced at all.
Used by the login page to skip itself when ``APP_PASSWORD`` is empty.
"""
return {"required": not auth_disabled()}
@router.post("/login", response_model=LoginResponse)
def login(body: LoginRequest, response: Response) -> LoginResponse:
if not verify_password(body.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="invalid_password"
)
expected_pw = os.getenv("APP_PASSWORD") or ""
if expected_pw:
response.set_cookie(
key=AUTH_COOKIE_NAME,
value=make_cookie_value(expected_pw),
httponly=True,
# HF Spaces render the app inside a cross-site iframe on
# huggingface.co, so the auth cookie must be SameSite=None to be
# sent with /api/* requests; that in turn requires Secure=True.
# The browser always sees HTTPS (HF proxy / Cloudflare tunnel both
# terminate TLS), so Secure works for every deployment.
samesite="none",
secure=True,
max_age=60 * 60 * 24 * 7,
path="/",
)
return LoginResponse(ok=True)
@router.post("/logout", response_model=LoginResponse)
def logout(response: Response) -> LoginResponse:
response.delete_cookie(AUTH_COOKIE_NAME, path="/")
return LoginResponse(ok=True)