L'envoi d'un code secret à usage unique (OTP) par SMS est une procédure courante pour la vérification du numéro de téléphone, la récupération de compte, l'authentification renforcée et la confirmation de paiement. Passer d'un navigateur à une application de messagerie SMS pour mémoriser ou copier-coller un code à plusieurs chiffres complique l'expérience utilisateur, augmente les taux d'abandon et expose les utilisateurs au hameçonnage sur des domaines usurpés.
En combinant le balisage sémantique <form>, <input autocomplete="one-time-code" inputmode="numeric">, la mise en forme des SMS liés à l'origine (@domain #code) et l'API WebOTP (OTPCredential), vous créez un flux de validation multicouche et progressivement amélioré :
- Extraction et envoi automatiques : les navigateurs compatibles avec l'API WebOTP vérifient l'origine par rapport à la ligne
@domain #codedu SMS, renseignent le<input>et déclenchent la validation d'un simple geste. - Saisie automatique au clavier liée à l'origine : les navigateurs et les systèmes d'exploitation compatibles avec
autocomplete="one-time-code"(comme Safari sur iOS, iPadOS et macOS) analysent le message@domain #codeet suggèrent le code dans la barre de saisie automatique au clavier uniquement lorsque le domaine correspond. - Saisie manuelle fluide : les utilisateurs qui saisissent un code depuis un autre appareil reçoivent un pavé numérique virtuel via
inputmode="numeric"sans les problèmes d'ergonomie de<input type="number">.
Considérations de sécurité
Les OTP par SMS permettent de vérifier la possession d'un numéro de téléphone et d'effectuer une validation renforcée pratique. Toutefois, le transport des SMS et les numéros de téléphone peuvent être sujets au SIM swapping, au recyclage de numéros et à l'interception. Associez toujours la saisie automatique côté client et l'API WebOTP au format SMS @domain #code lié à l'origine afin que le navigateur et l'OS vérifient que le domaine cible correspond avant de suggérer ou de fournir le code. Pour l'authentification principale résistante au hameçonnage, privilégiez les clés d'accès aux codes par SMS (voir Présentation des clés d'accès).
Balisage sémantique des formulaires
Encapsulez la commande de validation dans un élément <form> sémantique avec method="POST" et un <button type="submit"> explicite. Utilisez toujours un seul élément <input> plutôt que de répartir les chiffres dans plusieurs cases.
- Utilisez
type="text"avecinputmode="numeric": n'utilisez jamaistype="number"pour les codes OTP. Un code secret à usage unique est une chaîne d'identification qui peut contenir des zéros au début (par exemple,012345), et non une quantité dénombrable. L'utilisation detype="number"ajoute des boutons de sélection d'incrémentation/décrémentation, peut supprimer les zéros non significatifs et permet de modifier accidentellement la valeur de la molette de défilement. Évitez égalementtype="tel", qui est une solution de contournement obsolète destinée aux numéros de téléphone complets. Associeztype="text"àinputmode="numeric"pour afficher un clavier virtuel composé uniquement de chiffres sur les appareils mobiles. - Ajoutez
autocomplete="one-time-code": indique aux navigateurs et aux systèmes d'exploitation de suggérer les codes de validation par SMS entrants dans la barre de saisie automatique lorsque l'élément<input>est sélectionné. - Placez les instructions au-dessus de l'élément
<input>: associez un élément<label>visible à l'aide deforetid, et placez toutes les instructions de format (associées viaaria-describedby) visuellement au-dessus de l'élément<input>afin que les popovers de saisie automatique et les claviers virtuels mobiles ne masquent jamais les instructions. - Ne divisez pas les chiffres sur plusieurs éléments
<input>: les modèles de codes à usage unique à plusieurs cases (six entréesmaxlength="1"distinctes) interrompent la saisie automatique nativeautocomplete="one-time-code"dans de nombreux navigateurs, empêchent le copier-coller et compliquent la navigation avec un lecteur d'écran. Si une conception visuelle segmentée est requise, appliquez un style à un seul<input>avecletter-spacinget une police à chasse fixe.
<form id="otp-form" action="/verify-otp" method="POST">
<div class="form-group">
<label for="otp-input">Verification code</label>
<!-- Place the hint above the input so autofill popovers and mobile keyboards do not obscure it -->
<p id="otp-format-hint" class="field-hint">
Enter the 6-digit code sent to your phone.
</p>
<!-- MANDATORY: Use a single input with type="text", inputmode="numeric", and autocomplete="one-time-code" -->
<input
type="text"
id="otp-input"
name="otp"
inputmode="numeric"
autocomplete="one-time-code"
pattern="\d{6}"
maxlength="6"
aria-describedby="otp-format-hint"
required>
</div>
<button type="submit">Verify code</button>
</form>
/* Optional: Style a single input to visually space digits without splitting the DOM element */
#otp-input {
font-family: ui-monospace, monospace;
font-size: 1.25rem; /* Example value: must be at least 1rem (16px) to prevent mobile browser auto-zoom */
letter-spacing: 0.35em;
min-height: 48px;
padding: 0.5rem 0.75rem;
box-sizing: border-box;
}
Format des SMS liés à l'origine
Pour que l'API WebOTP et la saisie automatique autocomplete="one-time-code" liée au domaine de Safari puissent vérifier que le code appartient à votre site Web, votre service de SMS backend doit formater la dernière ligne du message avec le domaine d'origine précédé de @ et le mot de passe à usage unique précédé de # :
Your verification code is 123456.
@example.com #123456
Appliquez les règles de mise en forme suivantes au SMS sortant :
- Incluez d'abord du texte lisible par l'humain : commencez toujours le SMS par une phrase claire et lisible par l'humain contenant le code, afin que les utilisateurs qui le saisissent manuellement sur un autre appareil puissent le lire facilement.
- Placez
@<domain> #<otp>sur la toute dernière ligne : le caractère@doit être le premier caractère de la dernière ligne, sans aucune ligne ni texte de signature après. - N'utilisez que le domaine hôte : n'incluez pas de schéma d'URL (
https://ouhttp://), de port (:8080) ni de chemin d'accès (/verify) dans le jeton@domain. - Séparez les jetons par un seul espace : insérez exactement un espace entre
@<domain>et#<otp>, et n'incluez jamais d'espace dans le domaine ni entre#et le code.
Intégrer l'API WebOTP à votre flux de validation
L'API WebOTP (OTPCredential) nécessite un contexte sécurisé (https:// en production ou http://localhost lors du développement local). Au lieu de coller un script DOMContentLoaded déconnecté qui interroge le DOM et appelle form.submit(), intégrez navigator.credentials.get() directement dans le cycle de vie et le gestionnaire d'envoi de votre UI de validation :
- Associez le cycle de vie de la requête à l'étape de mot de passe à usage unique active : commencez à écouter le mot de passe à usage unique lorsque l'entrée de validation est affichée et active (par exemple, lorsque l'étape de mot de passe à usage unique est activée après que l'utilisateur a demandé un code par SMS, et chaque fois que l'utilisateur clique sur un bouton "Renvoyer le code").
- Gérer un
AbortControllersur tous les chemins de sortie : transmettez unAbortSignal(signal: ac.signal) ànavigator.credentials.get(). Appelez leac.abort()dans les cas suivants :- L'utilisateur envoie manuellement le formulaire (ou envoie un code renseigné via
autocomplete="one-time-code"). - Un nouveau code SMS est demandé ("Renvoyer le code"), ce qui annule le contrôleur précédent avant d'en créer un autre.
- Le composant, la boîte de dialogue ou la route de validation est démonté.
- L'utilisateur envoie manuellement le formulaire (ou envoie un code renseigné via
- Déclenchez le gestionnaire d'envoi de l'application (
form.requestSubmit()) : lorsquenavigator.credentials.get()se résout avecotp, définissezotpInput.value = otp.codeet appelezform.requestSubmit()(ou appelez directement votre fonction de validation). N'appelez PASform.submit(), carform.submit()contourne les écouteurs d'événementssubmit(gestionnairesevent.preventDefault()/fetch()) et ignore la validation des contraintes natives.
const otpForm = document.getElementById('otp-form');
const otpInput = document.getElementById('otp-input');
let otpAbortController = null;
function stopSmsOtpListener() {
if (otpAbortController) {
otpAbortController.abort();
otpAbortController = null;
}
}
async function startSmsOtpListener() {
// Feature-detect WebOTP support
if (!otpInput || !('OTPCredential' in window)) {
return;
}
// Cancel any previous pending WebOTP request (e.g., when re-sending an SMS code)
stopSmsOtpListener();
const ac = new AbortController();
otpAbortController = ac;
try {
const otp = await navigator.credentials.get({
otp: { transport: ['sms'] },
signal: ac.signal,
});
if (!otp) return;
otpInput.value = otp.code;
// Trigger standard form submission so submit event listeners and validation run
if (otpForm) {
otpForm.requestSubmit();
}
} catch (err) {
// Expected when aborted by manual submission, resend, unmount, or user dismissal
if (err && err.name === 'AbortError') {
return;
}
console.error('WebOTP verification error:', err);
}
}
otpForm.addEventListener('submit', async (event) => {
// MANDATORY: Abort any in-flight WebOTP request when the form is submitted
stopSmsOtpListener();
// Application-specific submission logic (e.g., AJAX fetch or native POST)
event.preventDefault();
const formData = new FormData(otpForm);
// await fetch(otpForm.action, { method: 'POST', body: formData });
});
// Start listening when the OTP verification step becomes active
startSmsOtpListener();
Validation des iFrames interorigines
La réception d'un OTP par SMS dans un <iframe> interorigine est limitée par défaut et est principalement utilisée pour les flux de confirmation de paiement intégrés (comme la validation 3D Secure, où shop.example intègre un test d'émetteur de bank.example). La prise en charge de WebOTP dans un <iframe> interorigine nécessite trois configurations coordonnées :
Associez les deux origines sur la dernière ligne de l'en-tête SMS : listez d'abord le domaine d'intégration de premier niveau (
@shop.example), suivi de#<otp>, puis d'un espace et du domaine de l'iFrame intégré (@bank.example) :Your verification code is 123456. @shop.example #123456 @bank.exampleEnvoyez l'en-tête de réponse HTTP
Permissions-Policy: déléguezotp-credentialsà l'origine de l'iframe interorigine dans les en-têtes HTTP du document parent :Permissions-Policy: otp-credentials=(self "https://bank.example")Ajoutez
allow="otp-credentials"à l'élément<iframe>: accordez explicitement la règle d'autorisation sur la balise<iframe>de la page d'intégration et incluez un attributtitledescriptif pour les lecteurs d'écran :
<iframe
src="https://bank.example/verify"
allow="otp-credentials"
title="Payment verification">
</iframe>
Dans le document <iframe> diffusé par https://bank.example, exécutez le même balisage <form> et le même cycle de vie de validation navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }) que ceux indiqués ci-dessus.
Stratégies de remplacement
Solution de secours de l'API WebOTP
Si vos navigateurs cibles ne sont pas compatibles avec l'API WebOTP (OTPCredential), ne chargez pas de polyfill JavaScript tiers, car la lecture des SMS entrants nécessite une intégration au niveau du système d'exploitation. Utilisez plutôt l'amélioration progressive à trois niveaux intégrée au balisage <input> :
- Expérience d'amélioration progressive : protégez l'appel
navigator.credentials.get({ otp: { transport: ['sms'] } })derrièreif ('OTPCredential' in window). LorsqueOTPCredentialn'est pas disponible :- Les navigateurs compatibles avec
autocomplete="one-time-code"(comme Safari sur iOS 14 ou version ultérieure, iPadOS et macOS avec le relais SMS iCloud) analysent le SMS@domain #codeet affichent le code dans la barre de saisie automatique du clavier système lorsque l'élément<input>est sélectionné. - Tous les autres navigateurs reviennent à la saisie manuelle dans l'élément
<input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required>unique, affichent un pavé numérique sur les appareils mobiles et envoient les données via l'élément<button type="submit">.
- Les navigateurs compatibles avec
Remplacement de la règle d'autorisation
Si le navigateur n'est pas compatible avec la délégation de otp-credentials via Permissions-Policy et <iframe allow="otp-credentials">, la validation intégrée <form> dans l'<iframe> interorigine se dégrade de manière fluide en suggestions de clavier autocomplete="one-time-code" et en saisie numérique manuelle sans générer d'exceptions non gérées.
Compatibilité avec inputmode et AbortController
L'attribut HTML inputmode et AbortController (AbortSignal) sont tous deux compatibles avec les navigateurs récents et ne nécessitent pas de code polyfill ni de remplacement.