Aller au contenu

Connexion privée renforcée avec TOTP

Billet #377 : Renforcement de la connexion à l'interface privée de l'application.
Type : Fonctionnalité et protection de l'accès privé.
Composants concernés : Connexion, accès au portefeuille, activation et récupération d'accès.

1. Contexte et objectifs

L'application dispose d'une interface privée qui contient mes véritables données de portefeuille d'investissement. La connexion reposait sur un identifiant et un mot de passe. Je souhaitais renforcer cette protection sans ajouter un parcours compliqué ni dépendre d'un service d'envoi de SMS ou de courriels.

L'objectif était qu'un mot de passe volé ne suffise plus pour accéder au portefeuille. J'ai retenu une double authentification TOTP : un code temporaire produit par une application d'authentification sur le téléphone, en complément du mot de passe.

Le choix reste proportionné à une application personnelle : un formulaire unique, une activation administrée et une récupération depuis le serveur en cas de perte du téléphone.

2. Les améliorations réalisées

J'ai ajouté le code temporaire au formulaire de connexion existant. Je saisis désormais mon identifiant, mon mot de passe et mon code d'authentification sur le même écran. Après activation, l'accès privé est accordé uniquement lorsque les deux facteurs sont valides.

Trois protections complètent ce parcours :

  • Les tentatives répétées sont ralenties. Cinq échecs d'authentification sur cinq minutes entraînent une attente de cinq minutes. Une limite supplémentaire s'applique aux tentatives provenant de la même origine.
  • Un code déjà accepté ne peut pas être réutilisé pendant sa période de validité. Deux demandes simultanées ne doivent pas permettre de se connecter deux fois avec ce même code.
  • Les anciennes connexions perdent leur accès lors de l'activation ou du remplacement du facteur. Je dois alors me reconnecter avec la protection courante.

Les pages du portefeuille, les transactions et le tableau de bord conservent leur fonctionnement après connexion. La page de démonstration publique reste indépendante de cet espace privé.

3. La fiabilité de l'accès et de la récupération

Comme l'ajout d'un second facteur change aussi la manière d'activer la protection et de récupérer l'accès, j'ai documenté séparément ces deux parcours.

Une activation confirmée avant sa mise en service

J'ai prévu une activation depuis le terminal administratif du serveur. J'enregistre la clé dans mon application d'authentification, puis confirme un premier code. La protection n'est déclarée active qu'après cette vérification.

Le secret utilisé pour produire les codes est chiffré côté serveur. Il ne figure ni dans les pages de connexion, ni dans les cookies du navigateur, ni dans les journaux d'authentification. Une erreur de vérification ne rétablit pas une connexion par mot de passe seul.

Une récupération sans affaiblir la protection

En cas de perte du téléphone, je conserve une procédure de remplacement depuis le serveur. Aucun écran public de récupération ni code de secours n'est ajouté.

Le facteur précédent reste actif jusqu'à confirmation du nouveau. Après remplacement, l'ancien facteur et les anciennes sessions ne donnent plus accès au portefeuille. Cette procédure suppose de conserver un accès administratif au serveur indépendant du téléphone perdu.

Les règles détaillées sont présentées dans la documentation de l'authentification et le guide d'activation et de remplacement.

4. Les vérifications et leurs limites

J'ai vérifié les parcours avec des données séparées du portefeuille réel. Les contrôles couvrent notamment les codes incorrects ou expirés, leur réutilisation, les demandes simultanées, les temporisations, l'activation, le remplacement et la perte d'accès des anciennes sessions.

Vérification Résultat local
Référence avant les changements 429 tests réussis et 83,66 % de couverture, mesurés sur une copie isolée de la version de référence.
Ensemble des tests automatisés 525 tests réussis, 7 ignorés, aucun échec.
Part du code couverte par les tests 85,28 %, contre 83,66 % au départ. Cette mesure porte sur les lignes, pas sur les branches, et ne garantit pas l'absence de défauts.
Service d'authentification 93,31 % de couverture des lignes.
Parcours dans Google Chrome 31 réussis, couvrant la connexion TOTP et les transactions sur une base locale isolée.
Persistance et demandes simultanées sur SQLite Réutilisation du code refusée, confirmation unique et conservation de l'état après redémarrage vérifiées.

La vérification TypeScript et le contrôle de qualité des fichiers navigateur modifiés ont également réussi. Le contrôle global conserve toutefois six erreurs dans des fichiers non modifiés. Le score de qualité calculé selon la formule du projet est de 92,31/100 ; les sept tests ignorés n'y sont pas comptés comme des succès.

Ces tests ignorés correspondent à six contrôles MariaDB indisponibles et à un contrôle de migration non applicable à SQLite. Les résultats locaux ne remplacent donc pas la vérification des demandes simultanées sur la base utilisée en production.

La revue a également révélé qu'un point-virgule dans le commentaire de la migration était interprété comme un séparateur SQL. Je l'ai remplacé par un point et ajouté une régression qui vérifie les instructions transmises à la connexion. J'ai aussi aligné la durée estimée en français et en anglais et adapté la constitution au bilan de jetons retenu pour mes rapports.

Le renseignement des crédits Copilot a ensuite révélé un test documentaire trop rigide : il exigeait encore des cellules vides. Je l'ai adapté pour vérifier l'égalité des données FR/EN, qu'elles soient renseignées ou non, sans imposer une ancienne valeur. Le coût, lorsqu'il est présent, doit aussi être cohérent entre les deux versions.

5. Incident de production, remise en service et activation

Après le déploiement de cette fonctionnalité, le site privé est devenu indisponible. L'investigation a confirmé une réponse 502 Bad Gateway sur le site et la page de connexion, tandis que la documentation restait accessible. Le conteneur applicatif redémarrait en boucle avant le lancement de Gunicorn ; le compteur dépassait 1 600 redémarrages.

Cause : une configuration de production incomplète

La variable SECRET_KEY était présente, mais TOTP_ENCRYPTION_KEY manquait dans l'environnement de production. Le service tentait de convertir cette valeur absente avec .encode("ascii"), provoquant une AttributeError. La validation ne vérifiait pas explicitement ce cas et n'interceptait pas cette exception. Les trente tentatives d'amorçage répétaient alors une erreur permanente, puis le processus quittait sans démarrer le serveur web. Nginx ne recevait donc aucune réponse exploitable de son backend.

La vérification en lecture seule a permis d'écarter une migration oubliée comme cause immédiate : la migration 0005 était enregistrée, les colonnes TOTP, la table de limitation et son index étaient présents, et MariaDB était accessible. La reproduction isolée a confirmé le cas exact : clé de signature présente et clé TOTP absente.

Cette différence de configuration explique aussi pourquoi les contrôles avaient réussi sans prévenir la panne. Les essais locaux fournissaient des clés générées ; les tests de configuration couvraient une clé TOTP vide ou malformée, mais pas son absence avec une clé de signature conservée. Le workflow du déploiement était vert : après le lancement Docker, il vérifiait des textes dans les fichiers, sans attendre une réponse HTTP ni contrôler la stabilité du processus. Le succès du déploiement ne prouvait donc pas la disponibilité du site.

Solution appliquée : rétablir le service sans réinitialiser le portefeuille

L'investigation initiale n'a modifié ni fichier ni donnée métier. Après autorisation de la remise en service ciblée, j'ai d'abord vérifié qu'aucun facteur TOTP actif ou en préparation n'exigeait de récupérer une ancienne clé. J'ai ensuite provisionné une clé de chiffrement persistante, indépendante de la clé de session, sans l'afficher, avec une sauvegarde séparée de la base et des permissions réservées à root.

Pour permettre également la connexion HTTPS, j'ai vérifié la configuration Nginx et le backend limité à 127.0.0.1:8000. Nginx constituant l'unique proxy, le réglage AUTH_PROXY_HOPS=1 permet désormais à Flask de reconnaître le protocole HTTPS transmis, sans faire confiance à un nom d'hôte fourni par un en-tête de proxy. L'exigence HTTPS et les protections des cookies restent actives.

Enfin, j'ai recréé uniquement le conteneur applicatif, sans recréer MariaDB ni ses volumes. La simulation préalable et le journal de démarrage ont confirmé zéro transaction ajoutée. Les contrôles suivants ont apporté les preuves de reprise :

Contrôle Résultat
Page de connexion HTTPS 200, certificat TLS vérifié et formulaire TOTP présent.
Accès privé sans session Redirection vers la connexion ; HTTP redirigé vers HTTPS.
Stabilité après disponibilité Deux minutes sans redémarrage, avec sept réponses HTTPS réussies.
Cookies et cache de connexion Secure, HttpOnly, SameSite=Lax et Cache-Control: no-store vérifiés.
Base et volumes Même conteneur MariaDB et même volume ; compteurs inchangés : 140 titres, 27 358 historiques et 402 transactions.
Migrations Cinq migrations enregistrées, aucune nouvelle migration appliquée.

Ces mesures établissent la reprise observée, pas une garantie de stabilité à long terme. Aucun correctif du code de validation ni renforcement du workflow de déploiement n'a été réalisé pendant cette intervention ciblée ; ces deux défauts restent à corriger avec une régression sur la clé absente et un contrôle de disponibilité réelle.

Activation : distinguer la clé serveur du facteur du compte

Après la reprise, une connexion sans code a réussi. Ce comportement s'expliquait par l'état du compte : le facteur TOTP n'était pas encore activé. La clé de chiffrement serveur ne remplace pas l'enregistrement du téléphone ; tant que totp_enabled est faux, le code autorise le mot de passe seul.

J'ai donc clarifié le guide d'activation et de remplacement en français et en anglais : vérifier status, lancer begin depuis un terminal SSH interactif, enregistrer la clé manuellement sur le téléphone, confirmer un code avant dix minutes, puis attendre le code suivant pour tester la connexion. La confirmation consomme ce premier code et révoque les anciennes sessions.

Le contrôle en lecture seule réalisé ensuite a constaté un compte avec un facteur actif et aucune préparation en attente. L'activation est donc confirmée en base. Ce constat ne constitue toutefois pas une nouvelle preuve de connexion réussie avec deux facteurs ni de refus du mot de passe seul après activation : ces parcours doivent être vérifiés séparément, sans publier de clé ni de code.

6. Leçons apprises

Cette intervention m'a permis de préciser cinq habitudes pour travailler plus efficacement avec Copilot :

  • Préciser ce qui doit être livré. Je liste dès le départ le code, les tests, les documents FR/EN, le wiki et le rapport, avec une preuve de vérification pour chacun.
  • Dire ce qu'il reste à faire. Plutôt qu'un simple « continue », je nomme les tâches restantes et le point d'arrêt, par exemple avant le déploiement.
  • Vérifier les consignes en cas d'oubli. Je contrôle les règles du projet et leurs contradictions avant de répéter une demande.
  • Partager le lien de l'échec. Pour un problème de tests ou de déploiement, je fournis le lien GitHub et le résultat attendu plutôt qu'un long journal copié.
  • Distinguer livraison et fonctionnement. Je vérifie la configuration réelle, la réponse HTTPS et l'activation du compte, plutôt que de déduire leur succès d'un workflow vert.

Ces habitudes visent à réduire les relances et les relectures inutiles ; le bilan ci-dessous conserve les données de consommation disponibles.

7. Bilan d'utilisation de jetons

Le suivi de consommation porte sur l'ensemble du billet #377 : préparation, réalisation, vérifications et documentation.

Les chiffres disponibles ci-dessous sont conservés ; aucune mesure supplémentaire de consommation n'est disponible pour l'incident du 3 novembre et sa documentation.

Élément suivi État du bilan
Durée de la session de travail (estimation) 3 heures
Crédits Copilot AI consommés 1041

Références