""" routes/auth.py — Flux d'authentification OIDC (Authorization Code Flow) avec Keycloak, via Authlib. 7 étapes documentées dans la rétro-documentation (RD-3) et le journal de bord : 1. Détection de l'absence de session (décorateur / vérification session) 2. Génération de l'URL d'autorisation (+ nonce anti-rejeu) 3. Redirection de l'utilisateur vers Keycloak 4. Retour sur /auth avec le code d'autorisation 5. Échange du code contre un token (authorize_access_token) 6. Vérification du token (signature, nonce, expiration — parse_id_token) 7. Création de la session applicative Flask """ import uuid from flask import Blueprint, redirect, url_for, session from config import Config from services import audit_service from services.auth_service import client_ip def register_oauth(oauth): """Enregistre le client OIDC Keycloak auprès d'Authlib (appelé depuis app.py).""" return oauth.register( name="keycloak", client_id=Config.OIDC_CLIENT_ID, client_secret=Config.OIDC_CLIENT_SECRET, server_metadata_url=Config.OIDC_DISCOVERY_URL, client_kwargs={"scope": "openid profile email"}, ) def init_auth_routes(oauth): # Le blueprint est créé ICI (et non au niveau du module) afin que # chaque appel de create_app() (production, ou une fois par test en # environnement pytest) produise un blueprint neuf. Un blueprint créé # au niveau module serait décoré une seule fois puis réutilisé tel # quel, ce que Flask refuse dès le second register_blueprint(). bp = Blueprint("auth", __name__) keycloak = register_oauth(oauth) @bp.route("/login") def login(): # Étape 2 : génération de l'URL d'autorisation avec un nonce nonce = uuid.uuid4().hex session["nonce"] = nonce redirect_uri = url_for("auth.auth_callback", _external=True, _scheme="https") # Étape 3 : redirection vers Keycloak return keycloak.authorize_redirect(redirect_uri, nonce=nonce) @bp.route("/auth") def auth_callback(): db = _get_db() # Étape 5 : échange du code contre un token token = keycloak.authorize_access_token() nonce = session.pop("nonce", None) # Étape 6 : vérification (signature + nonce) du token userinfo = keycloak.parse_id_token(token, nonce=nonce) # Étape 7 : création de la session applicative session["user"] = userinfo session["id_token"] = token.get("id_token") audit_service.log(db, "LOGIN", None, "SUCCESS", userinfo.get("sub"), client_ip()) return redirect("/") @bp.route("/logout") def logout(): db = _get_db() user = session.get("user") or {} id_token = session.get("id_token") audit_service.log(db, "LOGOUT", None, "SUCCESS", user.get("sub"), client_ip()) session.clear() return redirect( f"{Config.OIDC_LOGOUT_URL}" f"?post_logout_redirect_uri={Config.PUBLIC_BASE_URL}" f"&id_token_hint={id_token}" ) return bp def _get_db(): from flask import current_app return current_app.config["DB"]