Aller au contenu

L'authentification multi-facteurs dans Forge (forge-mvc-mfa)

Ce document explique ce que fait l'opt-in forge-mvc-mfa, ce qu'il expose, et comment on s'en sert.

Module extrait

Le code MFA a été extrait du cœur vers le paquet forge-mvc-mfa ; le cœur Forge n'en dépend pas.

Cet opt-in suit la version du cœur

Il n'a pas de cycle de maturité propre : sa version est celle du pyproject.toml racine (OPTINS-MATURITY-FOLLOWS-CORE-001).
Le secret TOTP est chiffré au repos via Fernet (SEC-MFA-SECRET-ENCRYPTION-001), et sa clé se tourne depuis MFA-KEY-ROTATION-001.

forge-mvc-mfa ajoute un second facteur d'authentification : TOTP (application d'authentification), codes de récupération, challenge à la connexion, revalidation et protections (anti-rejeu, rate-limit).

Le secret TOTP est chiffré au repos (Fernet) ; l'application décide où persister les facteurs et quand exiger le second facteur.

Clé de chiffrement obligatoire

Le secret TOTP est chiffré avec FORGE_MFA_SECRET_KEY (Fernet).

Démarrer sans cette variable lève MfaSecretKeyMissing : le chiffrement n'est pas optionnel.

1. Rôle du module

Le mot de passe seul ne suffit pas pour les actions sensibles.
L'opt-in ajoute un second facteur.

Il couvre quatre temps :

  • enrôlement : générer un secret TOTP, l'afficher en QR Code, confirmer le premier code ;
  • challenge : après le mot de passe, exiger un code TOTP avant d'ouvrir la session ;
  • revalidation : redemander le facteur avant une action critique (step-up) ;
  • récupération : des codes à usage unique si l'appareil TOTP est perdu.

Forge fournit les helpers et les contrats ; la persistance des facteurs et des codes reste applicative (ADR-008).

2. Installation

Prérequis : activez le venv du projet

Quelle que soit la source, installez dans le venv du projet :

source .venv/bin/activate

Lancé hors d'un venv, pip vise le Python système (Debian 12+, Ubuntu 23.04+),
protégé par PEP 668. Il refuse alors d'installer, pour ne pas écraser les paquets
gérés par apt, et affiche externally-managed-environment.
Le venv de projet créé par forge new n'a pas ce verrou.

Installer le paquet

A. Depuis PyPI (stable)

La dernière version publiée :

pip install --pre forge-mvc-mfa

B. Depuis Git (avant-garde)

Cœur puis opt-in depuis git, dans le venv du projet (l'opt-in trouve le cœur git déjà en place, sans version publiée sur PyPI) :

pip install "git+https://github.com/caucrogeGit/Forge.git@main"
pip install "git+https://github.com/caucrogeGit/Forge.git@main#subdirectory=packages/forge-mvc-mfa"
3. Mise en service

Installer le paquet ne suffit pas à le rendre opérationnel.
Voici les gestes propres à forge-mvc-mfa, dans l'ordre.

Ils déclinent la procédure canonique, Rendre un opt-in opérationnel : les cinq
points
.

1. L'épingler

forge-mvc-mfa==<version de forge-mvc>

Dans requirements.txt, à la même version ou au même commit que forge-mvc.
Sans cette ligne, l'opt-in n'existe que sur votre machine.

2. L'inscrire

forge opt-in:enable mfa --apply

L'opt-in est inscrit dans optins/registry.py (ADR-061), ce qui le rend visible du
projet.
--apply est obligatoire : sans lui, la commande simule et n'écrit rien.

3. Poser ce dont il a besoin

Rien à faire dans le cas courant.
Cet opt-in n'apporte aucune table, la persistance des facteurs appartenant à l'application.

Une seule exception, si vous servez l'authentification par plusieurs workers et voulez
un anti-rejeu TOTP commun à tous.
Le registre partagé, décrit plus bas, s'appuie alors sur une table.

forge mfa:init          # écrit la migration dans mvc/migrations/, sans l'exécuter
forge migration:apply   # après relecture

La déclaration de cette table vit dans tables.py, rendue pour le backend installé.

4. Le brancher là où il agit

Il se branche dans app.py, là où l'application compose ses middlewares et ses
fournisseurs de contexte. Ce câblage vous appartient : Forge ne l'écrit jamais à
votre place (principe 9).

5. Le prouver

make check
forge doctor

Puis un premier usage réel.
Un opt-in installé, inscrit et provisionné qu'aucun code n'appelle n'est pas
opérationnel : il est seulement présent.

4. Désinstallation
forge opt-in:disable mfa
pip uninstall forge-mvc-mfa

opt-in:disable est l'inverse d'enable : il dé-inscrit du registre, sans toucher au paquet.
forge opt-in:remove mfa affiche la commande pip uninstall sans l'exécuter.

5. Commandes

Cet opt-in n'expose aucune commande CLI : il s'utilise par import dans le code applicatif (voir l'API publique ci-dessous).

6. Vue d'ensemble rapide
Élément Valeur
Paquet forge-mvc-mfa
Module forge_mvc_mfa
Catégorie Sécurité et accès (ADR-055)
Couche opt-in (brique optionnelle), transversal au flux d'auth
Dépend de forge-mvc, pyotp, cryptography (Fernet)
Facteurs TOTP (MFA_FACTOR_TOTP), codes de récupération (MFA_FACTOR_RECOVERY)
Chiffrement secret TOTP chiffré (Fernet), clé FORGE_MFA_SECRET_KEY
Protections anti-rejeu TOTP, rate-limit du challenge et de la revalidation
API publique enrôlement, challenge, revalidation, codes de récupération, chiffrement
Persistance applicative (ADR-008) : AuthMfaFactor, codes de récupération
Installation pip install --pre forge-mvc-mfa
7. Schémas UML

Les deux schémas suivants montrent deux vues complémentaires de l'opt-in.

Le diagramme de classe montre les groupes d'API et le secret chiffré.

Le diagramme de séquence montre le challenge MFA à la connexion.

5.1 Diagramme de classe

Le diagramme de classe montre les fonctions groupées par rôle, le facteur persisté et le chiffrement du secret.

classDiagram
    direction LR

    class enrolment {
        <<module>>
        +generate_totp_secret() str
        +create_totp_factor(...)
        +confirm_totp_factor(...) AuthMfaFactor
        +totp_provisioning_uri(...) str
        +verify_totp_code(...)
    }

    class challenge {
        <<module>>
        +start_mfa_challenge(...)
        +verify_mfa_challenge(...)
        +require_recent_mfa(...)
        +verify_mfa_revalidation(...)
    }

    class recovery {
        <<module>>
        +create_recovery_codes(...)
        +consume_recovery_code(...)
    }

    class secret_crypto {
        <<module>>
        +encrypt_totp_secret(...)
        +decrypt_totp_secret(...)
        +validate_mfa_secret_key_config()
    }

    class AuthMfaFactor {
        <<dataclass>>
        +user_id
        +type
        +status
        +secret_chiffré
    }

    enrolment --> AuthMfaFactor : produit
    enrolment --> secret_crypto : chiffre le secret
    challenge --> AuthMfaFactor : vérifie
    recovery --> AuthMfaFactor : alternative TOTP

À retenir :

  • l'enrôlement produit un AuthMfaFactor (secret chiffré) ;
  • le challenge vérifie un code TOTP ou un code de récupération ;
  • la revalidation rejoue le facteur avant une action critique ;
  • le secret n'est jamais stocké en clair (Fernet).

5.2 Diagramme de séquence

Le diagramme de séquence montre le challenge à la connexion, après le mot de passe.

sequenceDiagram
    actor Utilisateur
    participant Login as Contrôleur login
    participant MFA as forge_mvc_mfa
    participant Session as Session

    Utilisateur->>Login: identifiants (mot de passe OK)
    Login->>MFA: is_mfa_enabled(user) ?
    alt MFA actif
        Login->>MFA: start_mfa_challenge(user)
        Login-->>Utilisateur: demande le code TOTP
        Utilisateur->>Login: code à 6 chiffres
        Login->>MFA: verify_mfa_challenge(code)
        MFA-->>Login: succès (ou code de récupération)
        Login->>Session: ouvre la session
    else MFA inactif
        Login->>Session: ouvre la session directement
    end

À retenir :

  • le challenge intervient après la vérification du mot de passe ;
  • la session n'est ouverte qu'une fois le second facteur validé ;
  • un code de récupération est une alternative au code TOTP ;
  • le challenge est limité en tentatives et en durée (anti-bruteforce).
8. API publique

Secret et chiffrement

Élément Rôle
generate_totp_secret() -> str génère un secret TOTP
encrypt_totp_secret / decrypt_totp_secret chiffre/déchiffre le secret (Fernet)
validate_mfa_secret_key_config() -> None vérifie FORGE_MFA_SECRET_KEY au démarrage
rotate_totp_secret(stored) -> str rechiffre un secret avec la clé courante
uses_current_key(stored) -> bool dit si un secret dépend encore d'une clé retirée
previous_keys() -> list[str] clés retirées déclarées, dans l'ordre

Enrôlement TOTP

Élément Rôle
create_totp_factor(...) crée un facteur TOTP en attente
confirm_totp_factor(...) -> AuthMfaFactor confirme avec le premier code
totp_provisioning_uri(...) -> str URI otpauth:// (pour QR Code)
verify_totp_code(...) vérifie un code TOTP
TotpSetup, AuthMfaFactor données d'enrôlement et facteur

Challenge et revalidation

Élément Rôle
start_mfa_challenge(...) démarre le challenge (état en session)
verify_mfa_challenge(...) vérifie le code du challenge
has_pending_mfa_challenge, clear_mfa_challenge état du challenge
require_recent_mfa(...) exige une revalidation récente (step-up)
mark_mfa_revalidated, verify_mfa_revalidation revalidation

Codes de récupération

Élément Rôle
create_recovery_codes(...) génère des codes à usage unique
consume_recovery_code(...) consomme un code (irréversible)

Constantes

MFA_FACTOR_TOTP, MFA_FACTOR_RECOVERY, MFA_STATUS_ACTIVE / PENDING / DISABLED, fenêtres et tentatives du challenge et de la revalidation.

9. Contextes d'utilisation
Besoin Élément
Vérifier la clé au démarrage validate_mfa_secret_key_config()
Enrôler un utilisateur create_totp_factor + totp_provisioning_uri + confirm_totp_factor
Fermer les sessions ouvertes à l'activation store.delete_for_user(...) du cœur
Exiger le 2e facteur au login start_mfa_challenge + verify_mfa_challenge
Protéger une action sensible require_recent_mfa(...)
Fournir un secours create_recovery_codes / consume_recovery_code
10. Exemple : challenge à la connexion
from forge_mvc_mfa import (
    is_mfa_enabled, start_mfa_challenge, verify_mfa_challenge,

)

# Après vérification du mot de passe. `factors` vient de votre stockage :
# c'est la liste des facteurs MFA de cet utilisateur.
if is_mfa_enabled(factors):
    start_mfa_challenge(request, user)
    return redirect("/login/mfa")     # demander le code TOTP

else:
    open_session(request, user)       # pas de MFA : session directe

# Sur la page de saisie du code :
if verify_mfa_challenge(request, request.form("code"), factors):
    open_session(request, user)

else:
    return Response.text("Code invalide", status=401)

factors est demandé partout où la décision en dépend, plutôt que rechargé en interne : Forge ne va pas chercher vos données à votre place, et le SQL de leur lecture reste chez vous (principe 5).

Aide-mémoire

Quatre temps, une clé de chiffrement :

  • enrôler (secret + QR + confirmation) ;
  • challenger au login ;
  • revalider avant le sensible ;
  • récupérer via codes à usage unique.
11. Fermer les sessions ouvertes à l'activation

Activer un second facteur ne protège rien tant que les sessions ouvertes avant l'activation restent valides.
Un accès obtenu avec le seul mot de passe survivrait au renforcement, ce qui vide le geste de son sens.

Le cœur fournit la primitive, et l'application l'appelle après avoir enregistré le facteur confirmé.

from core.sessions.access import get_session_id
from core.sessions.manager import get_session_store
from forge_mvc_mfa import confirm_totp_factor

actif = confirm_totp_factor(facteur, code_saisi)
if actif is None:
    return rendre_erreur("Code invalide.")

enregistrer_facteur(actif)

# Les autres sessions tombent, celle qui vient d'activer est épargnée.
get_session_store().delete_for_user(
    actif.user_id, except_session_id=get_session_id(request)
)

Épargner la session courante n'est pas facultatif

Sans except_session_id, l'utilisateur qui vient d'activer son facteur est déconnecté par son propre geste.
Cela ne protège de rien, et transforme un renforcement en panne apparente.

Pourquoi l'opt-in ne le fait pas lui-même

confirm_totp_factor est une fonction pure.
Elle ne touche ni la base ni les sessions, et rend un nouveau facteur sans modifier l'original.

L'appel reste donc explicite, à l'endroit où l'application enregistre le facteur.
Forge refuse la magie cachée (principe 3), et un opt-in qui fermerait des sessions à l'insu de l'appelant en serait.

12. Sécurité des secrets

Le secret TOTP est chiffré au repos avec Fernet (cryptography) et la clé FORGE_MFA_SECRET_KEY ; il n'est jamais stocké en clair.

Appelez validate_mfa_secret_key_config() au démarrage (app.py / wsgi.py) : démarrer sans clé valide échoue tôt plutôt qu'à la première écriture.

Codes de récupération à usage unique

Les codes de récupération sont stockés hachés et consommés une seule fois (consume_recovery_code).

Présentez-les une fois à l'utilisateur à la génération ; ils ne sont pas réaffichables.

Anti-rejeu et rate-limit

Un code TOTP déjà utilisé est refusé (anti-rejeu) ; le challenge et la revalidation sont limités en tentatives et en fenêtre temporelle.

Ces protections sont actives par défaut.

13. Politique de stockage des secrets MFA

Ce qui est en place

Le secret TOTP est chiffré au repos via Fernet (bibliothèque cryptography).

Le module est opt-in, non inclus dans forge-mvc[all], et doit être configuré avec FORGE_MFA_SECRET_KEY avant tout déploiement.

Développement et tests

En développement et en environnement de test isolé :

  • le secret TOTP est chiffré dans auth_mfa_factors.totp_secret (Fernet, préfixe enc:) ;
  • la clé de chiffrement est lue depuis FORGE_MFA_SECRET_KEY, requise même en dev ;
  • les codes de récupération sont stockés sous forme hashée (hash_recovery_code(), SHA-256 + secrets.compare_digest).

Conditions requises même en développement :

  • FORGE_MFA_SECRET_KEY positionné dans l'environnement ;
  • accès à la table auth_mfa_factors limité à l'utilisateur applicatif ;
  • secrets jamais loggés (totp_secret et recovery_code sont dans les champs redactés de sanitize_auth_audit_metadata()) ;
  • base de données non exposée publiquement.

Production

Le chiffrement Fernet est en place (SEC-MFA-SECRET-ENCRYPTION-001) et la rotation de clé l'est aussi (MFA-KEY-ROTATION-001).
Deux exigences restent à la charge de l'exploitant : la sauvegarde de la clé, dont la perte rend les secrets illisibles, et la revue de sécurité de son propre déploiement.

Protection additionnelle recommandée en production :

  • restreindre les droits d'accès à la table auth_mfa_factors au strict minimum applicatif ;
  • stocker FORGE_MFA_SECRET_KEY dans un gestionnaire de secrets (Vault, AWS Secrets Manager…) ;
  • appeler validate_mfa_secret_key_config() au démarrage applicatif (cf. section 7) ;
  • chiffrement du disque de la base de données ;
  • ne pas exporter auth_mfa_factors dans des dumps non chiffrés ;
  • documenter la procédure de rotation et de sauvegarde/restauration de la clé.

Secrets TOTP

Le secret TOTP est une clé partagée utilisée pour calculer les codes TOTP (RFC 6238).

Pourquoi on ne peut pas simplement hasher le secret TOTP :

Un hash est à sens unique.
Pour vérifier un code TOTP, le serveur doit pouvoir recalculer TOTP(secret, timestamp).
Si le secret est hashé, cette opération est impossible.

Le stockage production-ready d'un secret TOTP nécessite :

  • un chiffrement applicatif réversible (AES-256-GCM ou équivalent avec clé de chiffrement séparée), ou
  • un HSM (Hardware Security Module), ou
  • un gestionnaire de secrets (Vault, AWS Secrets Manager, ou équivalent).

Depuis SEC-MFA-SECRET-ENCRYPTION-001, forge-mvc-mfa implémente le chiffrement Fernet (cryptography.fernet.Fernet, AES-128-CBC + HMAC-SHA256) via la clé FORGE_MFA_SECRET_KEY.
Les valeurs stockées en base sont préfixées enc: pour distinguer les secrets chiffrés d'éventuelles valeurs legacy.

Pour renforcer davantage, coupler FORGE_MFA_SECRET_KEY à un gestionnaire de secrets externe.

Codes de récupération

Les codes de récupération sont correctement protégés dans forge-mvc-mfa :

  • générés via secrets.choice() sur un alphabet sans ambiguïté ;
  • hashés avant stockage via hash_recovery_code() (SHA-256) ;
  • vérifiés via secrets.compare_digest() (résistant aux timing attacks) ;
  • stockés en base uniquement sous forme de hash : le code brut n'est jamais persisté.

Cette conception est conforme pour la production, à condition que la base elle-même soit protégée.
Un hash de code de récupération exposé ne permet pas de retrouver le code brut.

Exigences avant production-ready

Ce qui relevait du paquet est livré. Ce qui reste relève de l'exploitant, et ne peut pas en relever autrement : Forge ne sait ni où vous sauvegardez vos clés, ni qui relit votre déploiement.

  1. Chiffrement applicatif des secrets TOTP ✓ livré (SEC-MFA-SECRET-ENCRYPTION-001) : Fernet + FORGE_MFA_SECRET_KEY.
  2. Politique de rotation ✓ livré (MFA-KEY-ROTATION-001) : FORGE_MFA_SECRET_KEY_PREVIOUS, rotate_totp_secret, uses_current_key.
  3. Tests dédiés au stockage chiffré ✓ livré (SEC-MFA-SECRET-ENCRYPTION-001) : tests/test_mfa_secret_crypto.py.
  4. Sauvegarde de la clé de chiffrement : à votre charge. Sa perte rend tous les secrets TOTP illisibles, et aucun facteur ne se revalide.
  5. Revue de sécurité de votre déploiement : à votre charge.

Tickets liés

Ticket Description État
MFA-SECRET-STORAGE-POLICY-001 Documenter la politique de stockage livré
SEC-MFA-SECRET-ENCRYPTION-001 Chiffrement applicatif du secret TOTP (Fernet) livré
MFA-PYPI-READY-001 Requalification Alpha (Pre-Alpha → Alpha) livré
14. Faire tourner la clé de chiffrement

Changer FORGE_MFA_SECRET_KEY sans précaution rend tous les secrets TOTP illisibles au même instant.
Chaque porteur d'un facteur perd alors son second facteur, et une mesure d'hygiène devient une panne d'authentification.

FORGE_MFA_SECRET_KEY_PREVIOUS déclare les clés retirées, séparées par des virgules.
Elles servent uniquement au déchiffrement, le chiffrement utilisant toujours la clé courante.

La procédure en quatre temps.

  1. Générer la nouvelle clé, sans encore la poser.

    python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
    
  2. Poser la nouvelle clé comme clé courante, et l'ancienne comme clé retirée.

    FORGE_MFA_SECRET_KEY=<nouvelle>
    FORGE_MFA_SECRET_KEY_PREVIOUS=<ancienne>
    

    À ce stade tout fonctionne, les anciens secrets restent lisibles et les nouveaux sont chiffrés avec la nouvelle clé.

  3. Rechiffrer les secrets existants, au rythme voulu.

    from forge_mvc_mfa import rotate_totp_secret, uses_current_key
    
    for facteur in mes_facteurs_totp():
        if uses_current_key(facteur.totp_secret):
            continue
        enregistrer(facteur.id, rotate_totp_secret(facteur.totp_secret))
    
  4. Retirer FORGE_MFA_SECRET_KEY_PREVIOUS une fois qu'aucun secret ne dépend plus de l'ancienne clé.

Pourquoi Forge ne balaie pas la base lui-même

La table des facteurs appartient à l'application, et Forge n'en connaît ni le nom ni les colonnes.
Le paquet fournit la primitive de rechiffrement, l'application décide où elle s'applique.

C'est le principe 1, qui sépare le framework de l'application métier.

Le secret en clair ne transite jamais

rotate_totp_secret travaille de jeton chiffré à jeton chiffré, via MultiFernet.rotate.
Le secret TOTP n'est ni rendu à l'appelant ni journalisé pendant la rotation.

Une clé retirée reste un secret

Tant que FORGE_MFA_SECRET_KEY_PREVIOUS est renseignée, sa valeur déchiffre des secrets réels.
Elle se protège comme la clé courante, et se retire dès la fin du rechiffrement.

Par défaut, l'anti-rejeu vaut par processus, pas par application

Le registre des codes déjà utilisés vit en mémoire du processus.
Derrière un serveur à plusieurs workers, gunicorn typiquement, chaque worker a le sien.
Un même code TOTP peut donc être accepté une fois par worker, soit jusqu'à autant de fois qu'il y a de workers.

La fenêtre est courte, un code TOTP vivant trente secondes, et l'attaquant doit déjà détenir le code.
Le risque réel est donc le rejeu d'un code intercepté, pas la découverte d'un code.

Cette note invoquait auparavant le rate-limit du challenge en atténuation.
C'était trompeur, et corrigé ici : ce compteur vit dans la même mémoire de processus, et souffre exactement du même défaut.
Le paragraphe suivant le dit.

Trois remèdes, au choix de l'exploitant.

  • Servir l'authentification par un seul worker, ce qui suffit à beaucoup d'applications.
  • Installer le registre partagé livré par Forge, voir le paragraphe suivant.
  • Porter le registre dans un magasin partagé de votre cru, en écrivant une classe conforme au protocole TotpReplayStore.

Le registre n'est pas non plus persisté : un redémarrage l'oublie, avec la même fenêtre de moins de trente secondes.

Le rate-limit du challenge vaut lui aussi par processus

MFA_CHALLENGE_MAX_ATTEMPTS vaut cinq essais par fenêtre de cinq minutes, et MFA_REVALIDATION_MAX_ATTEMPTS trois.

Ces compteurs vivent dans core.auth.rate_limit, en mémoire du processus.
Derrière quatre travailleurs Gunicorn, ce que l'unité systemd engendrée par Forge lance, les cinq essais en deviennent jusqu'à vingt, et le verrouillage ne suit pas l'attaquant d'un travailleur à l'autre.

La parade est la même que pour la connexion, et elle se pose au proxy, qui compte pour tous les travailleurs.
La configuration Nginx engendrée par forge deploy:init porte cette limite sur /login depuis DEPLOY-NGINX-RATE-LIMIT-001.

Le challenge MFA vit sur une autre route, que Forge ne connaît pas : le paquet ne pose aucune route, c'est l'application qui les écrit.
Ajoutez donc un location de même forme sur votre route de challenge, sans quoi elle reste la seule porte non bornée du parcours d'authentification.

Un compteur applicatif partagé reste possible sans que Forge en livre un : check_auth_rate_limit accepte une liste de tentatives chargée d'où vous voulez.

Partager le registre entre tous les processus

Forge livre DbTotpReplayStore, adossé au backend BDD du projet, donc commun à tous les workers.
Il ne s'active pas tout seul, l'application le pose au démarrage en une ligne visible.

from forge_mvc_mfa import set_replay_store
from forge_mvc_mfa.replay_store_db import DbTotpReplayStore

set_replay_store(DbTotpReplayStore())

La table se provisionne comme celle de tout opt-in adossé à la base.

forge mfa:init          # écrit la migration dans mvc/migrations/, sans l'exécuter
forge migration:apply   # après relecture

Cette table n'est requise que si vous installez ce registre.
Un projet qui garde le défaut n'a aucune table à créer, forge-mvc-mfa restant une bibliothèque sans persistance.

Le coût est d'une écriture par validation de code.
En contrepartie la garantie devient exacte, y compris entre processus, et elle n'exige ni Redis ni aucune dépendance nouvelle.

Persistance applicative

Forge fournit les helpers et les contrats (AuthMfaFactor, codes) ; l'application choisit la persistance (table, schéma), cohérent avec ADR-008.

Indépendance du cœur

Le cœur de Forge ne dépend pas de forge-mvc-mfa : la dépendance va de l'opt-in vers le cœur.

15. Rendre le facteur obligatoire pour un rôle

Le paquet savait dire si un utilisateur a un facteur actif. Il ne savait pas dire s'il devrait en avoir un (MFA-REQUIRED-BY-ROLE-001).

L'application écrivait donc, dans chaque contrôleur sensible, un « si cet utilisateur est administrateur et n'a pas de MFA, alors refuser ». Elle l'écrivait bien la première fois, et l'oubliait au troisième écran d'administration ajouté six mois plus tard.

MFA_REQUIRED_ROLES=admin,comptable
from forge_mvc_mfa.policy import check_mfa_requirement

verdict = check_mfa_requirement(session, facteurs_de_l_utilisateur)
if verdict.must_enroll:
    return BaseController.redirect_with_flash(request, "/mfa/setup", verdict.reason)

La politique n'active rien

Rendre un facteur obligatoire ne peut pas le créer à la place de l'utilisateur : il faut son téléphone, et son consentement.

La politique dit qu'un accès doit être refusé tant que le facteur manque ; c'est à l'application de conduire l'utilisateur vers l'inscription, et reason lui donne le message.

Le paquet ne connaît pas forge-mvc-rbac

Aucun opt-in n'importe un autre.

Les rôles sont lus dans la session, où l'authentification les a rangés, et la politique n'a pas besoin de savoir d'où ils viennent. Trois emplacements sont acceptés, user.roles, user.role et roles à la racine : les applications les emploient tous les trois, et n'en reconnaître qu'un ferait échouer la politique en silence, ce qui est la pire issue pour un contrôle de sécurité.

check_mfa_requirement ne lève jamais

Un contrôle de sécurité qui échoue en levant sur une session mal formée priverait d'accès un utilisateur légitime.

Il rend un verdict, et l'appelant décide.

Sans MFA_REQUIRED_ROLES, rien n'est obligatoire : le paquet n'impose pas une politique que personne n'a demandée. Les noms de rôles sont normalisés en minuscules, une majuscule ne devant pas faire échouer une politique.

Les fonctions vivent dans policy.py (check_mfa_requirement, required_roles, roles_of, is_mfa_required_for, MfaRequirement).

Voir aussi