30.10.26
Architecture OTP : sécuriser l’envoi SMS et e-mail14 min de lecture2 781 mots

Architecture OTP : sécuriser l’envoi SMS et e-mail

Concevez une architecture OTP fiable : séparez émission et envoi, gérez le repli SMS/e-mail et tracez les événements pour éviter les relances inutiles.

architecture OTPauthentification à deux facteursenvoi OTP par SMSrepli OTP par e-mailjournalisation des événements
Architecture OTP : sécuriser l’envoi SMS et e-mail

OTP par SMS et e-mail : concevoir une architecture fiable et auditable

À 3 h 07, une alerte signale une hausse des envois de secours par e-mail. Le tableau de bord du prestataire indique que les requêtes ont été acceptées, mais une question reste sans réponse : les SMS ont-ils vraiment échoué, ou leur confirmation est-elle juste en retard ? Et pourquoi des messages sont-ils encore envoyés à des coordonnées déjà déclarées invalides ?

Ce scénario pointe un problème classique avec l'authentification par code à usage unique (OTP). Le défi n'est pas seulement d'envoyer un SMS, puis un e-mail si nécessaire : c'est d'expliquer chaque étape. Quel canal a été tenté ? Sur quelle preuve s'appuie le repli ? Et pourquoi telle destination a été exclue (ou réutilisée) ? On peut répondre à ces questions avec une architecture simple, à condition de distinguer clairement l'émission du code, les tentatives de livraison, les événements fournis par le prestataire et les règles de suppression.

Pourquoi séparer le code de sa livraison

Un OTP est une preuve temporaire destinée à vérifier une étape d'authentification. Le fait de l'envoyer par SMS ou par e-mail sert de transport, et ce transport dépend de délais, d'incidents et de règles propres à chaque opérateur. Si l'application mélange « le code » et « sa livraison », elle perd vite la capacité de dire ce qui s'est réellement passé.

Quand l'API d'un prestataire répond « OK », cela signifie généralement qu'elle a accepté la demande. Ça ne prouve pas que le message est arrivé sur le téléphone, qu'il a été remis à la boîte de réception, ni que l'utilisateur l'a reçu à temps. Les événements suivants - livraison, rejet, rebond ou expiration - racontent une autre histoire. Le système doit garder cette séparation.

Un challenge, plusieurs tentatives possibles

Le modèle mental le plus simple : un challenge associé à une ou plusieurs tentatives de livraison. Le challenge décrit le cycle de vie de la vérification : création, durée de validité, consommation et politique applicable. Une tentative correspond à une décision d'envoi sur un canal, vers une destination donnée.

Par exemple : un challenge peut déclencher une tentative SMS. Si cette tentative se termine dans un état défini par la politique, ou si un délai est dépassé, le système peut alors évaluer une tentative e-mail - avec les mêmes règles, et en gardant une justification propre au repli. Dans ce modèle, le repli n'est pas une redirection automatique : c'est une nouvelle décision.

Pour une première version, quatre tables suffisent généralement :

Enregistrement Rôle Informations à préserver
Challenge Décrire le cycle de vie du code Heure de création et version de la politique
Tentative Décrire une décision d'envoi Canal, région, empreinte du destinataire et motif
Événement Conserver les preuves reçues Identifiant fournisseur, statut brut et heure observée
Suppression Bloquer une destination pour une cause précise Événement déclencheur et date de décision

Un curseur durable complète le tout pour ingérer les événements au bon endroit. Pas besoin de microservices « par table » : l'essentiel est la séparation des responsabilités et la conservation de l'historique utile.

Conserver l'historique au lieu d'écraser un statut

Mettre une seule colonne et la faire passer de en_attente à échec, puis à envoyé, paraît efficace. En pratique, ça efface les étapes précédentes. Si des notifications arrivent dans le désordre, ou si un événement est rejoué, vous finissez par perdre la trace de ce qui a déclenché le repli.

Gardez plutôt les événements en append-only : un événement s'ajoute, il ne remplace pas. L'état actuel d'une tentative se calcule ensuite à partir de l'historique. En cas de divergence entre ce que votre application affiche et ce que le fournisseur indique, vous disposez des identifiants et des horodatages nécessaires pour enquêter.

Et attention à un point souvent négligé : cette trace ne doit pas devenir un second stockage de secrets. N'enregistrez pas le code OTP dans le journal d'événements. Limitez sa durée de vie, contrôlez l'accès et séparez ce qui sert à vérifier les preuves de livraison de ce qui relève de l'authentification. Pour aller plus loin sur les contrôles liés aux mécanismes d'authentification, voyez le guide OWASP sur l'authentification multifacteur.

Comment décider d'un repli sans renvoyer vers une destination invalide

Un compte peut rester autorisé à se connecter alors qu'une de ses coordonnées ne doit plus recevoir de messages. C'est un point important : invalider un numéro ne devrait pas, à lui seul, désactiver le compte. De la même manière, l'échec d'un SMS ne doit pas automatiquement déclencher un e-mail, sans vérifier que cette autre destination est bien autorisée par la politique du challenge.

Avant chaque tentative, appliquez un ordre de décision explicite :

  1. Charger le challenge et vérifier qu'il n'est ni expiré ni consommé, et que ses limites de tentatives ne sont pas dépassées.
  2. Résoudre la destination vers une empreinte interne stable. Ne pas utiliser le numéro ou l'adresse en clair comme étiquette de métrique.
  3. Vérifier la suppression correspondant au canal et à cette destination.
  4. Créer la tentative uniquement si la destination est éligible.
  5. Si le canal principal atteint un état terminal défini ou dépasse son délai, évaluer le canal de repli avec les mêmes contrôles.
  6. Enregistrer la décision et son motif, y compris quand aucun message n'est envoyé.

Le point 6 est celui qui casse le plus souvent l'auditabilité. Pourtant, refuser une tentative est aussi une décision à tracer. Ça permet de répondre sans deviner : le système n'a-t-il rien envoyé parce que la destination était supprimée ? Parce que le challenge a expiré ? Ou parce que le nombre d'essais était atteint ?

Distinguer une erreur temporaire d'une destination invalide

Tous les échecs ne justifient pas une suppression. Un délai peut venir d'un incident temporaire, d'une congestion opérateur ou du fait qu'un événement de livraison n'a pas encore été reçu. À l'inverse, une preuve qu'une adresse n'existe pas (ou qu'un numéro est définitivement invalide) justifie souvent une suppression durable ou prolongée, selon la politique que vous définissez.

La transition doit aussi être idempotente : traiter deux fois le même événement ne doit ni créer deux suppressions contradictoires, ni réactiver une destination par accident. Conservez l'identifiant de l'événement à l'origine de la décision, la date et la règle appliquée. Cette trace rend la suppression explicable et facilite la révision si l'utilisateur met à jour ses coordonnées.

Côté implémentation, vous pouvez normaliser les motifs internes, par exemple : livré, échec_temporaire, destinataire_invalide, inconnu. Conservez aussi le statut brut du fournisseur. Un statut reçu « demain » ne doit pas être transformé silencieusement en échec permanent : une mauvaise classification entraîne soit un repli injustifié, soit l'empêchement d'un envoi légitime.

Éviter de divulguer les coordonnées dans les métriques

Pour suivre les tentatives sans exposer les coordonnées, utilisez une empreinte stable générée avec une méthode adaptée à votre modèle de menace. Une empreinte trop simple peut être devinée par dictionnaire, surtout pour des e-mails. L'accès aux données permettant de retrouver une destination doit rester limité et justifié.

En France et dans l'Union européenne, la minimisation, la durée de conservation et les droits d'accès ne devraient pas être ajoutés « après coup ». Le RGPD ne se résume pas à chiffrer une table : il faut savoir pourquoi une donnée est conservée, qui peut la consulter et quand elle est supprimée. Les décisions juridiques et de conformité fixent la politique ; l'architecture fournit les moyens de la respecter et de la vérifier.

Comment ingérer les événements de livraison de façon fiable

Pour démarrer, un collecteur par interrogation périodique peut suffire. À chaque cycle, il lit les événements à partir du dernier curseur validé, les normalise, puis enregistre les événements et met à jour le curseur dans une transaction. La règle simple : ne jamais avancer le curseur avant d'avoir durablement enregistré la page correspondante.

Si le processus s'arrête après l'enregistrement mais avant d'avoir confirmé la progression, il relira la même page. Une contrainte d'unicité sur le couple source, identifiant_evenement_fournisseur rend ce rejeu sans effet secondaire. Et non, vous n'avez pas besoin de promettre un « exactement une fois » de bout en bout : c'est rarement tenable. Le but, c'est un traitement rejouable et sûr.

Le contrat applicatif peut rester léger : le collecteur récupère une page d'événements et un curseur suivant ; le stockage garantit l'insertion idempotente et la mise à jour atomique du curseur. En cas d'échec d'enregistrement, le curseur ne bouge pas. En cas de rejeu de la page, les doublons sont reconnus via leurs identifiants.

Normaliser sans effacer la preuve d'origine

La normalisation convertit les statuts fournisseurs vers les catégories comprises par votre politique, mais elle doit garder la valeur d'origine. Vous aurez besoin de cette preuve si vos règles évoluent.

Les statuts inconnus, eux, doivent être conservés et signalés pour examen. Ne les classez pas automatiquement comme « échec ». C'est précisément le genre de raccourci qui fait déclencher des décisions de repli quand ce n'était pas le bon diagnostic.

Stockez aussi l'heure fournie par la source et l'heure de réception par votre application. Ce sont deux questions différentes : quand le fournisseur dit que l'événement s'est produit, et quand votre système l'a réellement observé. Cet écart aide à mesurer le retard d'ingestion et à expliquer pourquoi une décision a été prise avant d'avoir vu la confirmation.

Choisir entre interrogation périodique et notifications poussées

L'interrogation périodique limite le nombre de composants et rend le rejeu plus lisible. Son coût, c'est une latence : un événement peut rester inconnu jusqu'au prochain cycle. Choisissez la fréquence en fonction des limites du fournisseur, du volume et du délai acceptable pour décider d'un repli - pas en partant du principe que tout arrive « tout de suite ».

Si vous avez besoin d'une décision plus rapide que la fréquence d'interrogation, ajoutez des notifications poussées (authentifiées, avec des protections contre les rejeux, la disponibilité et la reprise). Dans ce cas, une approche pragmatique consiste à recevoir les notifications pour accélérer le traitement, puis garder l'interrogation par curseur comme mécanisme de réconciliation. Ce n'est pas obligatoire partout : augmentez la complexité quand le besoin de latence est réel, mesuré.

Le curseur lui-même doit être durable et surveillé. Une stagnation prolongée peut vouloir dire que le collecteur est arrêté, qu'une page échoue en boucle ou que l'API fournisseur a changé. Surveillez l'âge du dernier événement traité et le temps écoulé depuis la dernière progression, pas seulement un état « actif ».

Quels indicateurs suivre et comment déployer progressivement

Les tableaux de bord du fournisseur servent pendant un incident, mais ils ne vous disent pas si votre application a vérifié la suppression avant le routage. Les alertes prioritaires doivent donc porter sur vos décisions et sur les invariants de votre système.

Commencez par suivre :

  • les tentatives autorisées pour une destination déjà supprimée (alerte rapide : c'est une violation de règle) ;
  • le taux de repli par challenge, canal et région ;
  • les événements classés comme destinataire définitivement invalide ;
  • le retard d'ingestion et la stagnation du curseur ;
  • les statuts inconnus et les tentatives présentant des preuves terminales contradictoires ;
  • le nombre de challenges expirés, consommés ou bloqués par leurs limites de tentatives.

Préférez des dimensions à faible cardinalité : canal, région, motif normalisé. Évitez d'utiliser des adresses, des numéros ou des empreintes individuelles comme étiquettes de métriques : c'est un risque de confidentialité et ça fait exploser les séries temporelles. Gardez ces détails dans un stockage d'audit, avec des accès contrôlés.

Alerter sans transformer chaque variation en incident

Alerter sur chaque OTP échoué crée du bruit. À l'inverse, attendre que « le volume double » peut faire passer longtemps des tentatives vers des destinations invalides. Séparez donc les seuils par gravité : alerte immédiate en cas de contournement d'une suppression, puis alerte de tendance sur la hausse des replis, des rejets permanents ou du retard d'ingestion.

Ajustez les seuils à partir de volumes réels. Trop strict, et vous finissez par ignorer l'alerte. Trop permissif, et vous détectez tard. Chaque changement de seuil doit être consigné avec sa justification : sinon, vous aurez du mal à comprendre pourquoi l'équipe n'alerte plus... ou pourquoi elle alerte soudainement tout le temps.

Testez avec des événements synthétiques : doublon, ordre inversé, rejeu d'un curseur, statut inconnu, expiration du SMS principal, suppression du canal de repli et exécution simultanée de plusieurs travailleurs. Ce ne sont pas des cas théoriques : ils valident l'historique, l'idempotence et la robustesse des décisions dans des conditions « normales » en production.

Préparer les messages et le déploiement

Le contenu joue aussi dans la livraison. Pour les SMS, l'encodage influence le découpage en segments : certains caractères basculent dans des alphabets différents. Gardez le texte court, testez l'encodage réel du modèle et versionnez l'artefact envoyé (le modèle/templating), plutôt que d'enregistrer le corps rendu contenant le code. Un caractère inattendu peut augmenter le nombre de segments, donc le coût et le délai.

Pour les e-mails, la configuration d'expédition compte. SPF, DKIM et DMARC contribuent à l'authentification et à la réputation du domaine, mais leur configuration dépend notamment du volume et des exigences du fournisseur. Et surtout : un e-mail « accepté » par l'API n'est pas une preuve de réception. Les rebonds ensuite doivent alimenter la classification et, si nécessaire, déclencher une suppression de la destination.

Déployez progressivement : commencez par un seul fournisseur et un seul canal principal, puis ajoutez le repli avec ses règles séparées. Vérifiez les événements de bout en bout en environnement de test, puis comparez les décisions de l'application aux preuves du prestataire. Le bon critère n'est pas le nombre de composants déployés : c'est votre capacité à répondre vite à la question « Pourquoi ce message est-il parti ? ».

Conclusion : rendre chaque décision explicable

Une architecture OTP fiable ne dépend pas d'un tableau de bord plus sophistiqué. Elle tient sur des frontières claires : un challenge séparé de ses tentatives, une vérification de suppression avant chaque envoi, des événements conservés sans écrasement et un curseur qui ne progresse qu'après une validation durable.

Commencez par ces invariants, puis instrumentez le taux de repli, les destinations invalides et le retard d'ingestion. Testez les rejeux et les événements désordonnés avant de complexifier le transport. Et si, pendant un incident, vous devez « reconstruire l'histoire » à partir d'une cellule de statut mutable ou d'un écran du prestataire, l'auditabilité n'est pas encore suffisante.

Prochaine étape : vérifiez votre flux actuel de bout en bout et repérez la première décision qui n'est pas justifiée par une preuve conservée. C'est presque toujours là que le chantier doit commencer.

Questions frequentes

Non. Elle indique généralement que le fournisseur a accepté la demande. La livraison doit être confirmée par un événement ultérieur, lorsqu’il est disponible. Conservez séparément l’acceptation de l’API et les événements de livraison : les confondre conduit à surestimer le taux de réception et à prendre de mauvaises décisions de repli.
Non. Définissez les états qui autorisent le repli et vérifiez séparément la validité du challenge, les limites d’essais et la suppression de l’adresse e-mail. Un délai temporaire ne prouve pas nécessairement que le SMS a échoué définitivement. Le repli doit être une décision enregistrée, pas une redirection implicite.
Non. Le journal doit expliquer les décisions de livraison, pas conserver un secret d’authentification. Enregistrez les identifiants d’événement, les statuts, les horodatages et les versions de règles utiles. Le code doit rester dans le mécanisme de vérification, avec une durée de vie limitée et des accès restreints.
Elle suffit si son délai est compatible avec le parcours utilisateur et les objectifs opérationnels. Si la décision de repli doit arriver plus vite, ajoutez des notifications poussées authentifiées pour le chemin rapide, puis gardez l’interrogation comme réconciliation. Ce choix ajoute des composants et des tests ; mesurez le besoin avant de le faire.

Articles similaires