""" 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 from ..dependencies import get_redis_client from ..services.auth_service import AuthService router = APIRouter() security = HTTPBearer(auto_error=False) def get_auth_service(): """Get auth service instance.""" return AuthService(get_redis_client()) class RevokeUserRequest(BaseModel): """Request to revoke all tokens for a user.""" username: str l2_server: str @router.get("") async def auth_callback( request: Request, auth_token: str = None, auth_service: AuthService = Depends(get_auth_service), ): """ 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. """ if not auth_token: return RedirectResponse(url="/", status_code=302) # Verify the token is valid ctx = await auth_service.verify_token_with_l2(auth_token) if not ctx: return RedirectResponse(url="/", status_code=302) # Register token for this user (for revocation by username later) auth_service.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), auth_service: AuthService = Depends(get_auth_service), ): """ Revoke a token. Called by L2 when user logs out. The token to revoke is passed in the Authorization header. """ if not credentials: raise HTTPException(401, "No token provided") token = credentials.credentials # Verify token is valid before revoking (ensures caller has the token) ctx = auth_service.get_user_context_from_token(token) if not ctx: raise HTTPException(401, "Invalid token") # Revoke the token newly_revoked = auth_service.revoke_token(token) return {"revoked": True, "newly_revoked": newly_revoked} @router.post("/revoke-user") async def revoke_user_tokens( request: RevokeUserRequest, auth_service: AuthService = Depends(get_auth_service), ): """ 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. """ # Revoke all tokens registered for this user count = auth_service.revoke_all_user_tokens(request.username) return { "revoked": True, "tokens_revoked": count, "username": request.username }