Een eenmalige toegangscode (OTP) via sms sturen is een veelgebruikte methode voor verificatie van telefoonnummer, accountherstel, stapsgewijze verificatie en betalingsbevestiging. Als gebruikers moeten schakelen tussen een browser en een berichten-app om een code van meerdere cijfers te onthouden of te kopiëren en te plakken, leidt dit tot frictie, hogere verlatingspercentages en kwetsbaarheid voor phishing op gespoofde domeinen.
Door semantische <form>-opmaak, <input autocomplete="one-time-code" inputmode="numeric">, aan de oorsprong gebonden sms-opmaak (@domain #code) en de WebOTP API (OTPCredential) te combineren, maak je een gelaagd, progressief verbeterd verificatieproces:
- Automatische extractie en indiening: Browsers die de WebOTP API ondersteunen, verifiëren de oorsprong aan de hand van de regel
@domain #codein de sms, vullen de<input>in en activeren de verificatie met één tik. - Automatisch invullen via toetsenbord gebonden aan oorsprong: Browsers en besturingssystemen die
autocomplete="one-time-code"ondersteunen (zoals Safari op iOS, iPadOS en macOS) parseren het@domain #code-bericht en stellen de code alleen voor in de balk voor automatisch invullen via het toetsenbord als het domein overeenkomt. - Probleemloze handmatige invoer: Gebruikers die een code van een ander apparaat invoeren, krijgen een numeriek virtueel toetsenblok via
inputmode="numeric"zonder de gebruiksproblemen van<input type="number">.
Beveiligingsoverwegingen
Met een eenmalig wachtwoord via sms wordt het bezit van een telefoonnummer geverifieerd en wordt een handige stapsgewijze verificatie geboden, maar sms-overdracht en telefoonnummers kunnen vatbaar zijn voor simswaps, hergebruik van nummers en onderschepping. Koppel automatisch invullen aan de clientzijde en de WebOTP API altijd aan de @domain #code-indeling voor sms-berichten die aan de oorsprong zijn gebonden, zodat de browser en het OS verifiëren of het doeldomein overeenkomt voordat de code wordt voorgesteld of geleverd. Geef voor primaire phishingbestendige verificatie de voorkeur aan toegangssleutels boven sms-codes (zie Oriëntatie op toegangssleutels).
Semantische formulieropmaak
Plaats het verificatie-element in een semantisch <form>-element met method="POST" en een expliciete <button type="submit">. Gebruik altijd één <input>-element in plaats van cijfers over meerdere vakken te verdelen.
- Gebruik
type="text"metinputmode="numeric": Gebruik nooittype="number"voor OTP-codes. Een eenmalige toegangscode is een ID-tekenreeks die voorloopnullen kan bevatten (zoals012345) en geen telbare hoeveelheid. Als jetype="number"gebruikt, worden er spinnerknoppen voor verhogen/verlagen toegevoegd, kunnen voorloopnullen worden verwijderd en kunnen de waarden per ongeluk worden gewijzigd met het scrollwiel. Vermijd ooktype="tel", een verouderde tijdelijke oplossing die is bedoeld voor volledige telefoonnummers. Koppeltype="text"aaninputmode="numeric"om een virtueel toetsenbord met alleen cijfers te tonen op mobiele apparaten. - Add
autocomplete="one-time-code": Geeft browsers en besturingssystemen de opdracht om inkomende sms-verificatiecodes voor te stellen in de balk voor automatisch invullen als de<input>is geselecteerd. - Plaats instructietips boven de
<input>: Koppel een zichtbare<label>metforeniden plaats opmaakinstructies (gekoppeld viaaria-describedby) visueel boven de<input>, zodat pop-overs voor automatisch invullen en mobiele virtuele toetsenborden de instructies nooit bedekken. - Splits cijfers niet over meerdere
<input>-elementen: OTP-patronen met meerdere vakken (6 afzonderlijkemaxlength="1"-invoeren) verstoren de systeemeigen functie voor automatisch invullen vanautocomplete="one-time-code"in veel browsers, belemmeren kopiëren en plakken en bemoeilijken de navigatie met schermlezers. Als een gesegmenteerd visueel ontwerp vereist is, stijl je één<input>metletter-spacingen een monospace-lettertype.
<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;
}
Indeling voor sms-berichten die aan de oorsprong zijn gebonden
Als je wilt dat zowel de WebOTP API als de domeingebonden autocomplete="one-time-code"-functie voor automatisch invullen van Safari verifiëren dat de code bij je website hoort, moet je sms-service aan de backend de laatste regel van het tekstbericht opmaken met het oorspronkelijke domein voorafgegaan door @ en de OTP voorafgegaan door #:
Your verification code is 123456.
@example.com #123456
Handhaaf de volgende opmaakregels voor het uitgaande sms-bericht:
- Zet eerst door mensen leesbare tekst: Begin de sms altijd met een duidelijke, door mensen leesbare zin die de code bevat, zodat gebruikers die de code handmatig op een ander apparaat typen, deze makkelijk kunnen lezen.
- Plaats
@<domain> #<otp>op de allerlaatste regel: Het teken@moet het eerste teken van de laatste regel zijn, zonder volgende regels of handtekeningtekst erna. - Gebruik alleen het hostdomein: Neem geen URL-schema (
https://ofhttp://), poort (:8080) of pad (/verify) op in de@domain-token. - Scheid tokens met één spatie: Plaats precies één spatie tussen
@<domain>en#<otp>en gebruik nooit witruimte in het domein of tussen#en de code.
De WebOTP API integreren in je verificatieproces
De WebOTP API (OTPCredential) vereist een beveiligde context (https:// in productie of http://localhost tijdens lokale ontwikkeling). In plaats van een losstaand DOMContentLoaded-script te plakken dat de DOM opvraagt en form.submit() aanroept, integreert u navigator.credentials.get() rechtstreeks in de levenscyclus en de indieningshandler van uw verificatie-UI:
- Koppel de levenscyclus van het verzoek aan de actieve OTP-stap: Begin te luisteren naar het OTP als de verificatie-invoer wordt weergegeven en actief is (bijvoorbeeld als de OTP-stap wordt geactiveerd nadat de gebruiker een sms-code heeft aangevraagd en opnieuw wanneer de gebruiker op de knop Code opnieuw sturen klikt).
- Een
AbortControllerbeheren voor alle uitstappaden: Geef eenAbortSignal(signal: ac.signal) door aannavigator.credentials.get(). Belac.abort()wanneer je wilt:- De gebruiker dient het formulier handmatig in (of dient een code in die is ingevuld via
autocomplete="one-time-code"). - Er wordt een nieuwe sms-code aangevraagd (Code opnieuw sturen). De vorige controller wordt afgebroken voordat er een nieuwe wordt gemaakt.
- De verificatiecomponent, het dialoogvenster of de route wordt ontkoppeld.
- De gebruiker dient het formulier handmatig in (of dient een code in die is ingevuld via
- De indieningshandler van de app activeren (
form.requestSubmit()): Alsnavigator.credentials.get()wordt omgezet inotp, stel jeotpInput.value = otp.codein en roep jeform.requestSubmit()aan (of roep je de verificatiefunctie rechtstreeks aan). Roep NIETform.submit()aan, omdatform.submit()submit-event-listeners (event.preventDefault()-/fetch()-handlers) omzeilt en de validatie van systeemeigen beperkingen overslaat.
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();
Cross-origin iframe-verificatie
Een sms-eenmalig wachtwoord krijgen in een cross-origin <iframe> is standaard beperkt en wordt voornamelijk gebruikt voor ingesloten bevestigingsflows voor betalingen (zoals 3D Secure-verificatie waarbij shop.example een uitdaging van de kaartuitgever van bank.example insluit). Voor ondersteuning van WebOTP in een cross-origin <iframe> zijn 3 gecoördineerde configuraties vereist:
Bind beide oorsprongen op de laatste regel van de sms: Vermeld eerst het ingesloten domein op het hoogste niveau (
@shop.example), gevolgd door#<otp>, gevolgd door een spatie en het ingesloten iframe-domein (@bank.example):Your verification code is 123456. @shop.example #123456 @bank.exampleStuur de HTTP-reactieheader
Permissions-Policy: Delegeerotp-credentialsnaar de cross-origin iframe-oorsprong in de HTTP-headers van het bovenliggende document:Permissions-Policy: otp-credentials=(self "https://bank.example")Voeg
allow="otp-credentials"toe aan het element<iframe>: Verleen het rechtenbeleid expliciet aan de tag<iframe>op de ingesloten pagina en voeg een beschrijvend kenmerktitletoe voor schermlezers:
<iframe
src="https://bank.example/verify"
allow="otp-credentials"
title="Payment verification">
</iframe>
Voer in het <iframe>-document dat wordt geleverd door https://bank.example dezelfde <form>-opmaak en navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal })-verificatielevenscyclus uit als hierboven.
Reservestrategieën
WebOTP API-reserveoptie
Als je doelbrowsers de WebOTP API (OTPCredential) niet ondersteunen, laad je geen JavaScript-polyfill van derden, omdat voor het lezen van inkomende sms-berichten integratie op besturingssysteemniveau is vereist. Vertrouw in plaats daarvan op de progressive enhancement met 3 niveaus die is ingebouwd in de <input>-opmaak:
- Progressive enhancement: Beveilig de aanroep van
navigator.credentials.get({ otp: { transport: ['sms'] } })achterif ('OTPCredential' in window). AlsOTPCredentialniet beschikbaar is:- Browsers die
autocomplete="one-time-code"ondersteunen (zoals Safari op iOS 14+, iPadOS en macOS met iCloud-sms-relay) parseren de@domain #code-sms en tonen de code in de balk voor automatisch invullen van het systeemtoetsenbord als de<input>is geselecteerd. - Alle andere browsers vallen terug op handmatige invoer in het enkele
<input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required>, waarbij een numeriek toetsenblok wordt getoond op mobiele apparaten en wordt ingediend via de<button type="submit">.
- Browsers die
Fallback voor rechtenbeleid
Als de browser de overdracht van otp-credentials via Permissions-Policy en <iframe allow="otp-credentials"> niet ondersteunt, wordt de ingesloten verificatie <form> in de cross-origin <iframe> op een goede manier gedegradeerd naar autocomplete="one-time-code"-toetsenbordsuggesties en handmatige numerieke invoer zonder onverwerkte uitzonderingen te genereren.
Ondersteuning voor inputmode en AbortController
Zowel het HTML-kenmerk inputmode als AbortController (AbortSignal) wordt ondersteund in moderne browsers en vereist geen polyfill- of fallback-code.