L'invio di un passcode monouso (OTP) tramite SMS è un flusso comune per la verifica del numero di telefono, il recupero dell'account, l'autenticazione step-up e la conferma del pagamento. Passare da un browser a un'app di messaggistica SMS per memorizzare o copiare e incollare un codice di più cifre crea attrito per l'utente, aumenta i tassi di abbandono e rende gli utenti vulnerabili al phishing su domini contraffatti.
La combinazione di markup semantico <form>, <input autocomplete="one-time-code" inputmode="numeric">, formattazione degli SMS associata all'origine (@domain #code) e API WebOTP (OTPCredential) crea un flusso di verifica a più livelli con potenziamento progressivo:
- Estrazione e invio automatici: i browser che supportano l'API WebOTP verificano l'origine rispetto alla riga
@domain #codenell'SMS, compilano il campo<input>e attivano la verifica con un solo tocco. - Compilazione automatica della tastiera associata all'origine: i browser e i sistemi operativi che supportano
autocomplete="one-time-code"(come Safari su iOS, iPadOS e macOS) analizzano il messaggio@domain #codee suggeriscono il codice nella barra di compilazione automatica della tastiera solo quando il dominio corrisponde. - Inserimento manuale semplificato: gli utenti che inseriscono un codice da un altro dispositivo ricevono un tastierino virtuale numerico tramite
inputmode="numeric"senza i rischi di usabilità di<input type="number">.
Considerazioni sulla sicurezza
La password OTP via SMS verifica il possesso di un numero di telefono e fornisce una comoda verifica aggiuntiva, ma il trasporto degli SMS e i numeri di telefono possono essere soggetti a scambio di SIM, riciclo dei numeri e intercettazione. Abbina sempre la compilazione automatica lato client e l'API WebOTP al formato SMS @domain #code associato all'origine, in modo che il browser e la verifica del sistema operativo verifichino che il dominio di destinazione corrisponda prima di suggerire o inviare il codice. Per l'autenticazione principale resistente al phishing, preferisci le passkey ai codici basati su SMS (consulta la sezione Orientamento sulle passkey).
Markup semantico dei moduli
Racchiudi il controllo di verifica in un elemento semantico <form> con method="POST" e un <button type="submit"> esplicito. Utilizza sempre un singolo elemento <input> anziché suddividere le cifre in più caselle.
- Utilizza
type="text"coninputmode="numeric": non utilizzare maitype="number"per i codici OTP. Un passcode monouso è una stringa di identificazione che può contenere zeri iniziali (ad esempio012345), non una quantità numerabile. L'utilizzo ditype="number"aggiunge pulsanti di selezione per l'incremento/decremento, può rimuovere gli zeri iniziali e consente modifiche accidentali dei valori con la rotellina di scorrimento. Evita anchetype="tel", che è una soluzione alternativa legacy destinata ai numeri di telefono completi. Associatype="text"ainputmode="numeric"per presentare una tastiera virtuale solo numerica sui dispositivi mobili. - Aggiungi
autocomplete="one-time-code": indica ai browser e ai sistemi operativi di suggerire i codici di verifica degli SMS in arrivo nella barra di compilazione automatica quando l'elemento<input>è attivo. - Posiziona i suggerimenti didattici sopra l'elemento
<input>: associa un elemento<label>visibile utilizzandoforeide posiziona le istruzioni di formattazione (collegate tramitearia-describedby) visivamente sopra l'elemento<input>in modo che i popover di compilazione automatica e le tastiere virtuali mobile non coprano mai le istruzioni. - Non suddividere le cifre in più elementi
<input>: i pattern OTP con più caselle (sei inputmaxlength="1"separati) interrompono la compilazione automatica nativa diautocomplete="one-time-code"in molti browser, ostacolano il copia e incolla e complicano la navigazione con lo screen reader. Se è richiesto un design visivo segmentato, applica lo stile a un singolo<input>conletter-spacinge un carattere monospazio.
<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;
}
Formato dei messaggi SMS associati all'origine
Affinché sia l'API WebOTP sia la compilazione automatica di autocomplete="one-time-code" associata al dominio di Safari verifichino che il codice appartenga al tuo sito web, il servizio SMS di backend deve formattare l'ultima riga del messaggio con il dominio di origine preceduto da @ e l'OTP preceduto da #:
Your verification code is 123456.
@example.com #123456
Applica le seguenti regole di formattazione al messaggio SMS in uscita:
- Includi prima il testo leggibile: inizia sempre l'SMS con una frase chiara e leggibile contenente il codice, in modo che gli utenti che lo digitano manualmente su un altro dispositivo possano leggerlo facilmente.
- Posiziona
@<domain> #<otp>nell'ultima riga: il carattere@deve essere il primo carattere dell'ultima riga, senza righe finali o testo della firma dopo. - Utilizza solo il dominio ospitato: non includere uno schema URL (
https://ohttp://), una porta (:8080) o un percorso (/verify) nel token@domain. - Separa i token con un solo spazio: inserisci esattamente uno spazio tra
@<domain>e#<otp>e non includere mai spazi vuoti all'interno del dominio o tra#e il codice.
Integrazione dell'API WebOTP nel flusso di verifica
L'API WebOTP (OTPCredential) richiede un contesto sicuro (https:// in produzione o http://localhost durante lo sviluppo locale). Anziché incollare uno script DOMContentLoaded scollegato che esegue query sul DOM e chiama form.submit(), integra navigator.credentials.get() direttamente nel ciclo di vita e nel gestore di invio della UI di verifica:
- Collega il ciclo di vita della richiesta al passaggio OTP attivo: inizia ad ascoltare l'OTP quando l'input di verifica viene visualizzato e attivato (ad esempio, quando il passaggio OTP viene attivato dopo che l'utente richiede un codice SMS e ogni volta che l'utente fa clic sul pulsante "Invia di nuovo il codice").
- Gestisci un
AbortControllerin tutti i percorsi di uscita: passa unAbortSignal(signal: ac.signal) anavigator.credentials.get(). Chiamaac.abort()quando:- L'utente invia manualmente il modulo (o invia un codice compilato tramite
autocomplete="one-time-code"). - Viene richiesto un nuovo codice SMS ("Invia di nuovo il codice"), interrompendo il controller precedente prima di crearne uno nuovo.
- Il componente, la finestra di dialogo o il percorso di verifica vengono smontati.
- L'utente invia manualmente il modulo (o invia un codice compilato tramite
- Attiva il gestore di invio dell'applicazione (
form.requestSubmit()): quandonavigator.credentials.get()si risolve conotp, impostaotpInput.value = otp.codee chiamaform.requestSubmit()(o richiama direttamente la funzione di verifica). NON chiamareform.submit(), perchéform.submit()bypassa i listener di eventisubmit(gestorievent.preventDefault()/fetch()) e salta la convalida dei vincoli nativi.
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();
Verifica degli iframe multiorigine
La ricezione di un SMS OTP all'interno di un <iframe> multiorigine è limitata per impostazione predefinita e viene utilizzata principalmente per i flussi di conferma dei pagamenti incorporati (ad esempio la verifica 3D Secure in cui shop.example incorpora una verifica dell'emittente da bank.example). Il supporto di WebOTP all'interno di un <iframe> multiorigine richiede tre configurazioni coordinate:
Associa entrambe le origini nell'ultima riga dell'SMS: elenca prima il dominio di incorporamento di primo livello (
@shop.example), seguito da#<otp>, seguito da uno spazio e dal dominio iframe incorporato (@bank.example):Your verification code is 123456. @shop.example #123456 @bank.exampleInvia l'intestazione della risposta HTTP
Permissions-Policy: delegaotp-credentialsall'origine dell'iframe multiorigine nelle intestazioni HTTP del documento principale:Permissions-Policy: otp-credentials=(self "https://bank.example")Aggiungi
allow="otp-credentials"all'elemento<iframe>: concedi esplicitamente la policy sulle autorizzazioni nel tag<iframe>nella pagina di incorporamento e includi un attributotitledescrittivo per gli screen reader:
<iframe
src="https://bank.example/verify"
allow="otp-credentials"
title="Payment verification">
</iframe>
All'interno del documento <iframe> pubblicato da https://bank.example, esegui lo stesso markup <form> e lo stesso ciclo di vita di verifica navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }) mostrati sopra.
Strategie di riserva
Fallback dell'API WebOTP
Se i browser di destinazione non supportano l'API WebOTP (OTPCredential), non caricare un polyfill JavaScript di terze parti, poiché la lettura degli SMS in arrivo richiede l'integrazione a livello di sistema operativo. Affidati invece al potenziamento progressivo a tre livelli integrato nel markup <input>:
- Esperienza di potenziamento progressivo: proteggi la chiamata
navigator.credentials.get({ otp: { transport: ['sms'] } })dietroif ('OTPCredential' in window). QuandoOTPCredentialnon è disponibile:- I browser che supportano
autocomplete="one-time-code"(come Safari su iOS 14 e versioni successive, iPadOS e macOS con inoltro SMS di iCloud) analizzano l'SMS@domain #codee presentano il codice nella barra di compilazione automatica della tastiera di sistema quando<input>è attivo. - Tutti gli altri browser tornano all'inserimento manuale nel singolo
<input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required>, visualizzando un tastierino numerico sui dispositivi mobili e inviando i dati tramite<button type="submit">.
- I browser che supportano
Fallback della policy Permissions
Se il browser non supporta la delega di otp-credentials tramite Permissions-Policy e <iframe allow="otp-credentials">, la verifica incorporata <form> all'interno dell'elemento <iframe> multiorigine esegue un graceful degradation ai suggerimenti della tastiera autocomplete="one-time-code" e all'inserimento manuale dei numeri senza generare eccezioni non gestite.
Supporto di inputmode e AbortController
Sia l'attributo HTML inputmode sia AbortController (AbortSignal) sono supportati nei browser moderni e non richiedono codice polyfill o di riserva.