# backend/routers/sso.py # Fusion ACC-3.1 (#216) — Points d'entrée SSO de l'émettrice. # # GET /sso/authorize?return_to= : point d'entrée des autres # applications — valide le return_to (liste blanche *.dracodev.net, # anti open-redirect) puis redirige vers la page de connexion # d'ObsiGate en conservant le paramètre ; après connexion, le # frontend repart vers l'application demanderesse (le cookie # draco_session posé à la connexion couvre tout le domaine parent). # # GET /api/sso/revocations : liste des jti révoqués, réservée aux # autres applications (clé machine DRACO_SSO_MACHINE_KEY, en-tête # X-Draco-Machine-Key) qui la mettent en cache 60 s (design §5.2). # # GET /sso/logout?return_to= : symétrique de /sso/authorize — # point d'entrée navigable de la déconnexion globale (BUG-122, # fusion #141). Les autres applications y naviguent depuis leur menu # compte ; la déconnexion exécutée est celle de POST /api/auth/logout # (logique partagée : jetons natifs révoqués, jti draco révoqué, # cookie effacé sur le domaine parent), puis redirection vers le # return_to validé (même liste blanche ; repli « / »). from __future__ import annotations import os from urllib.parse import quote from fastapi import APIRouter, HTTPException, Request from fastapi.responses import RedirectResponse from backend import draco_session from backend.auth.router import apply_global_logout router = APIRouter(tags=["SSO"]) @router.get("/sso/authorize") async def sso_authorize(return_to: str = ""): target = draco_session.validate_return_to(return_to) if target: return RedirectResponse( url=f"/?return_to={quote(target, safe='')}", status_code=302) return RedirectResponse(url="/", status_code=302) @router.get("/sso/logout") async def sso_logout(request: Request, return_to: str = ""): target = draco_session.validate_return_to(return_to) response = RedirectResponse(url=target or "/", status_code=302) apply_global_logout(request, response) return response @router.get("/api/sso/revocations") async def sso_revocations(request: Request): machine_key = os.environ.get("DRACO_SSO_MACHINE_KEY", "") if not machine_key: raise HTTPException(404, "Non configuré") if request.headers.get("X-Draco-Machine-Key") != machine_key: raise HTTPException(401, "Clé machine invalide") return {"revoked": draco_session.revoked_list()}