ai-helpdesk-api / backend /auth_cookie.py
ritesh19180's picture
Upload folder using huggingface_hub
4e68ae5 verified
Raw
History Blame Contribute Delete
5.81 kB
"""HttpOnly cookie session bridge for Supabase JWTs (issue #130)."""
from __future__ import annotations
import os
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from pydantic import BaseModel, Field
ACCESS_COOKIE = "access_token"
REFRESH_COOKIE = "refresh_token"
ACCESS_MAX_AGE = 60 * 60
REFRESH_MAX_AGE = 60 * 60 * 24 * 7
router = APIRouter(prefix="/auth", tags=["auth"])
def _cookie_kwargs() -> dict[str, Any]:
secure = os.getenv("ENV", "production").lower() != "development"
return {
"httponly": True,
"secure": secure,
"samesite": "strict",
"path": "/",
}
def _anon_supabase():
from supabase import create_client
url = os.environ.get("SUPABASE_URL")
key = os.environ.get("SUPABASE_ANON_KEY") or os.environ.get("SUPABASE_SERVICE_KEY")
if not url or not key:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Auth backend not configured (SUPABASE_URL / SUPABASE_ANON_KEY missing)",
)
return create_client(url, key)
def extract_token(request: Request) -> str | None:
"""Prefer HttpOnly cookie; fall back to Authorization bearer header."""
cookie_token = request.cookies.get(ACCESS_COOKIE)
if cookie_token:
return cookie_token
auth = request.headers.get("authorization") or request.headers.get("Authorization")
if auth and auth.lower().startswith("bearer "):
return auth.split(" ", 1)[1].strip() or None
return None
def _set_session_cookies(response: Response, session: Any) -> None:
if not session or not getattr(session, "access_token", None):
return
response.set_cookie(
ACCESS_COOKIE,
session.access_token,
max_age=ACCESS_MAX_AGE,
**_cookie_kwargs(),
)
refresh = getattr(session, "refresh_token", None)
if refresh:
response.set_cookie(
REFRESH_COOKIE,
refresh,
max_age=REFRESH_MAX_AGE,
**_cookie_kwargs(),
)
def _clear_session_cookies(response: Response) -> None:
kwargs = _cookie_kwargs()
response.delete_cookie(ACCESS_COOKIE, path=kwargs["path"])
response.delete_cookie(REFRESH_COOKIE, path=kwargs["path"])
async def get_current_user(request: Request) -> dict:
token = extract_token(request)
if not token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
try:
client = _anon_supabase()
result = client.auth.get_user(token)
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Invalid session: {exc}",
) from exc
user = getattr(result, "user", None) or (result.get("user") if isinstance(result, dict) else None)
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid session")
if hasattr(user, "model_dump"):
return user.model_dump()
if hasattr(user, "dict"):
return user.dict()
return dict(user)
class LoginBody(BaseModel):
email: str = Field(min_length=3)
password: str = Field(min_length=1)
class SignupBody(BaseModel):
email: str = Field(min_length=3)
password: str = Field(min_length=6)
full_name: str | None = None
role: str | None = "user"
company: str | None = None
@router.post("/login")
async def auth_login(body: LoginBody, response: Response):
try:
client = _anon_supabase()
result = client.auth.sign_in_with_password(
{"email": str(body.email), "password": body.password}
)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=str(exc)) from exc
session = getattr(result, "session", None)
user = getattr(result, "user", None)
if not session or not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
_set_session_cookies(response, session)
user_payload = user.model_dump() if hasattr(user, "model_dump") else dict(user)
return {"user": user_payload, "message": "Session cookies set"}
@router.post("/signup")
async def auth_signup(body: SignupBody, response: Response):
metadata: dict[str, str] = {}
if body.full_name:
metadata["full_name"] = body.full_name
if body.role:
metadata["role"] = body.role
if body.company:
metadata["company"] = body.company
try:
client = _anon_supabase()
result = client.auth.sign_up(
{
"email": str(body.email),
"password": body.password,
"options": {"data": metadata} if metadata else {},
}
)
except Exception as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
session = getattr(result, "session", None)
user = getattr(result, "user", None)
if session:
_set_session_cookies(response, session)
user_payload = user.model_dump() if user and hasattr(user, "model_dump") else None
return {"user": user_payload, "message": "Signup complete"}
@router.post("/logout")
async def auth_logout(request: Request, response: Response):
# Invalidate the session server-side before clearing cookies
token = extract_token(request)
if token:
try:
client = _anon_supabase()
client.auth.sign_out(token)
except Exception:
pass # Still clear cookies even if server-side invalidation fails
_clear_session_cookies(response)
return {"ok": True}
@router.get("/me")
async def auth_me(user: dict = Depends(get_current_user)):
return {"user": user}