""" Authentication routes for L1 server. L1 doesn't handle login directly - users log in at their L2 server. Token is passed via URL from L2 redirect, then L1 sets its own cookie. """ from fastapi import APIRouter, Depends, HTTPException, Request from fastapi.responses import RedirectResponse from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from pydantic import BaseModel # Import auth utilities from existing server module # TODO: Move these to a service import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) router = APIRouter() security = HTTPBearer(auto_error=False) class RevokeUserRequest(BaseModel): """Request to revoke all tokens for a user.""" username: str l2_server: str @router.get("") async def auth_callback(auth_token: str = None): """ Receive auth token from L2 redirect and set local cookie. This enables cross-subdomain auth on iOS Safari which blocks shared cookies. L2 redirects here with ?auth_token=... after user logs in. """ # Import here to avoid circular imports from server import get_verified_user_context, register_user_token if not auth_token: return RedirectResponse(url="/", status_code=302) # Verify the token is valid ctx = await get_verified_user_context(auth_token) if not ctx: return RedirectResponse(url="/", status_code=302) # Register token for this user (for revocation by username later) register_user_token(ctx.username, auth_token) # Set local first-party cookie and redirect to runs response = RedirectResponse(url="/runs", status_code=302) response.set_cookie( key="auth_token", value=auth_token, httponly=True, max_age=60 * 60 * 24 * 30, # 30 days samesite="lax", secure=True ) return response @router.get("/logout") async def logout(): """ Logout - clear local cookie and redirect to home. Note: This only logs out of L1. User should also logout from L2. """ response = RedirectResponse(url="/", status_code=302) response.delete_cookie("auth_token") return response @router.post("/revoke") async def revoke_token( credentials: HTTPAuthorizationCredentials = Depends(security), ): """ Revoke a token. Called by L2 when user logs out. The token to revoke is passed in the Authorization header. """ from server import get_user_context_from_token, revoke_token as do_revoke_token if not credentials: raise HTTPException(401, "No token provided") token = credentials.credentials # Verify token is valid before revoking (ensures caller has the token) ctx = get_user_context_from_token(token) if not ctx: raise HTTPException(401, "Invalid token") # Revoke the token newly_revoked = do_revoke_token(token) return {"revoked": True, "newly_revoked": newly_revoked} @router.post("/revoke-user") async def revoke_user_tokens(request: RevokeUserRequest): """ Revoke all tokens for a user. Called by L2 when user logs out. This handles the case where L2 issued scoped tokens that differ from L2's own token. """ from server import revoke_all_user_tokens # Revoke all tokens registered for this user count = revoke_all_user_tokens(request.username) return { "revoked": True, "tokens_revoked": count, "username": request.username }