87 lines
3.1 KiB
Python
87 lines
3.1 KiB
Python
"""
|
|
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"]
|