Mengirimkan kode sandi sekali pakai (OTP) melalui SMS adalah alur umum untuk verifikasi nomor telepon, pemulihan akun, autentikasi langkah, dan konfirmasi pembayaran. Beralih antara browser dan aplikasi pesan SMS untuk menghafal atau menyalin-menempel kode multi-digit akan menimbulkan hambatan bagi pengguna, meningkatkan rasio berhenti, dan membuat pengguna rentan terhadap phishing di domain palsu.
Menggabungkan markup <form> semantik, <input autocomplete="one-time-code" inputmode="numeric">, pemformatan SMS terikat origin (@domain #code), dan WebOTP API (OTPCredential) akan menciptakan alur verifikasi berlapis yang ditingkatkan secara progresif:
- Ekstraksi dan pengiriman otomatis: Browser yang mendukung WebOTP API memverifikasi origin terhadap baris
@domain #codedalam SMS, mengisi<input>, dan memicu verifikasi dengan sekali ketuk. - Isi otomatis keyboard yang terikat asal: Browser dan sistem operasi yang mendukung
autocomplete="one-time-code"(seperti Safari di iOS, iPadOS, dan macOS) mengurai pesan@domain #codedan menyarankan kode di panel isi otomatis keyboard hanya jika domainnya cocok. - Entri manual yang lancar: Pengguna yang memasukkan kode dari perangkat lain akan menerima keypad virtual numerik melalui
inputmode="numeric"tanpa risiko kegunaan<input type="number">.
Pertimbangan keamanan
OTP SMS memverifikasi kepemilikan nomor telepon dan memberikan verifikasi langkah tambahan yang praktis, tetapi pengiriman SMS dan nomor telepon dapat menjadi sasaran pertukaran SIM, daur ulang nomor, dan penyadapan. Selalu pasangkan isi otomatis sisi klien dan WebOTP API dengan format SMS @domain #code yang terikat origin agar browser dan OS memverifikasi bahwa domain target cocok sebelum menyarankan atau mengirimkan kode. Untuk autentikasi utama yang tahan phishing, pilih kunci sandi daripada kode berbasis SMS (lihat Orientasi Kunci Sandi).
Markup formulir semantik
Gabungkan kontrol verifikasi dalam elemen <form> semantik dengan method="POST" dan <button type="submit"> eksplisit. Selalu gunakan satu elemen <input>, bukan memisahkan digit di beberapa kotak.
- Gunakan
type="text"denganinputmode="numeric": Jangan pernah gunakantype="number"untuk kode OTP. Kode sandi sekali pakai adalah string ID yang dapat berisi angka nol di awal (seperti012345), bukan jumlah yang dapat dihitung. Penggunaantype="number"akan menambahkan tombol spinner penambahan/pengurangan, dapat menghapus angka nol di awal, dan memungkinkan perubahan nilai roda scroll yang tidak disengaja. Hindari jugatype="tel", yang merupakan solusi lama yang ditujukan untuk nomor telepon lengkap. Pasangkantype="text"denganinputmode="numeric"untuk menampilkan keyboard virtual khusus angka di perangkat seluler. - Tambahkan
autocomplete="one-time-code": Menginstruksikan browser dan sistem operasi untuk menyarankan kode verifikasi SMS yang masuk di panel isi otomatis saat<input>difokuskan. - Tempatkan petunjuk instruksional di atas
<input>: Kaitkan<label>yang terlihat menggunakanfordanid, dan tempatkan petunjuk format apa pun (ditautkan melaluiaria-describedby) secara visual di atas<input>sehingga popover isi otomatis dan keyboard virtual seluler tidak pernah menutupi petunjuk. - Jangan pisahkan digit di beberapa elemen
<input>: Pola OTP multi-kotak (enam inputmaxlength="1"terpisah) merusak isi otomatisautocomplete="one-time-code"native di banyak browser, menghambat salin-tempel, dan mempersulit navigasi pembaca layar. Jika diperlukan desain visual tersegmentasi, beri gaya pada satu<input>denganletter-spacingdan font monospace.
<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 pesan SMS terikat origin
Agar WebOTP API dan isi otomatis autocomplete="one-time-code" yang terikat domain Safari dapat memverifikasi bahwa kode tersebut milik situs Anda, layanan SMS backend Anda harus memformat baris terakhir pesan teks dengan domain asal yang diawali dengan @ dan OTP yang diawali dengan #:
Your verification code is 123456.
@example.com #123456
Terapkan aturan pemformatan berikut pada pesan SMS keluar:
- Sertakan teks yang dapat dibaca manusia terlebih dahulu: Selalu mulai SMS dengan kalimat yang jelas dan dapat dibaca manusia yang berisi kode, sehingga pengguna yang mengetiknya secara manual di perangkat lain dapat membacanya dengan mudah.
- Tempatkan
@<domain> #<otp>di baris terakhir: Karakter@harus menjadi karakter pertama dari baris terakhir, tanpa baris tambahan atau teks tanda tangan setelahnya. - Hanya gunakan domain host: Jangan sertakan skema URL (
https://atauhttp://), port (:8080), atau jalur (/verify) dalam token@domain. - Pisahkan token dengan satu spasi: Tempatkan tepat satu spasi di antara
@<domain>dan#<otp>, dan jangan pernah menyertakan spasi kosong di dalam domain atau di antara#dan kode.
Mengintegrasikan WebOTP API ke dalam alur verifikasi Anda
WebOTP API (OTPCredential) memerlukan Konteks Aman (https:// dalam produksi, atau http://localhost selama pengembangan lokal). Daripada menempelkan skrip DOMContentLoaded yang tidak terhubung yang mengkueri DOM dan memanggil form.submit(), integrasikan navigator.credentials.get() langsung ke dalam siklus proses dan pengendali pengiriman UI verifikasi Anda:
- Kaitkan siklus proses permintaan ke langkah OTP aktif: Mulai proses pemantauan OTP saat input verifikasi dirender dan aktif (misalnya, saat langkah OTP dipasang setelah pengguna meminta kode SMS, dan lagi setiap kali pengguna mengklik tombol "Kirim ulang kode").
- Mengelola
AbortControllerdi semua jalur keluar: TeruskanAbortSignal(signal: ac.signal) kenavigator.credentials.get(). Hubungiac.abort()kapan saja:- Pengguna mengirimkan formulir secara manual (atau mengirimkan kode yang diisi melalui
autocomplete="one-time-code"). - Kode SMS baru diminta ("Kirim ulang kode"), yang membatalkan pengontrol sebelumnya sebelum membuat pengontrol baru.
- Komponen, dialog, atau rute verifikasi dilepas.
- Pengguna mengirimkan formulir secara manual (atau mengirimkan kode yang diisi melalui
- Memicu pengendali pengiriman aplikasi (
form.requestSubmit()): Saatnavigator.credentials.get()di-resolve denganotp, tetapkanotpInput.value = otp.codedan panggilform.requestSubmit()(atau panggil fungsi verifikasi Anda secara langsung). JANGAN panggilform.submit(), karenaform.submit()mengabaikan pemroses peristiwasubmit(pengendalievent.preventDefault()/fetch()) dan melewati validasi batasan native.
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();
Verifikasi iframe lintas origin
Menerima OTP SMS di dalam <iframe> lintas origin dibatasi secara default dan terutama digunakan untuk alur konfirmasi pembayaran tersemat (seperti verifikasi 3D Secure tempat shop.example menyematkan tantangan penerbit dari bank.example). Mendukung WebOTP di dalam <iframe> lintas origin memerlukan tiga konfigurasi terkoordinasi:
Ikat kedua origin di baris terakhir SMS: Cantumkan domain penyematan tingkat teratas terlebih dahulu (
@shop.example), diikuti dengan#<otp>, diikuti dengan spasi dan domain iframe yang disematkan (@bank.example):Your verification code is 123456. @shop.example #123456 @bank.exampleKirim header respons HTTP
Permissions-Policy: Delegasikanotp-credentialske origin iframe lintas origin di header HTTP dokumen induk:Permissions-Policy: otp-credentials=(self "https://bank.example")Tambahkan
allow="otp-credentials"ke elemen<iframe>: Berikan kebijakan izin secara eksplisit pada tag<iframe>di halaman penyematan dan sertakan atributtitledeskriptif untuk pembaca layar:
<iframe
src="https://bank.example/verify"
allow="otp-credentials"
title="Payment verification">
</iframe>
Di dalam dokumen <iframe> yang ditayangkan oleh https://bank.example, jalankan markup <form> dan siklus proses verifikasi navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }) yang sama seperti yang ditunjukkan di atas.
Strategi penggantian
Penggantian WebOTP API
Jika browser target Anda tidak mendukung WebOTP API (OTPCredential), jangan muat polyfill JavaScript pihak ketiga, karena membaca pesan SMS yang masuk memerlukan integrasi tingkat sistem operasi. Sebagai gantinya, andalkan progressive enhancement tiga tingkat yang disertakan dalam markup <input>:
- Pengalaman progressive enhancement: Lindungi panggilan
navigator.credentials.get({ otp: { transport: ['sms'] } })di balikif ('OTPCredential' in window). JikaOTPCredentialtidak tersedia:- Browser yang mendukung
autocomplete="one-time-code"(seperti Safari di iOS 14+, iPadOS, dan macOS dengan relai SMS iCloud) mengurai SMS@domain #codedan menampilkan kode di panel isi otomatis keyboard sistem saat<input>difokuskan. - Semua browser lainnya akan kembali ke entri manual di satu
<input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required>, menampilkan keypad numerik di perangkat seluler, dan mengirimkan melalui<button type="submit">.
- Browser yang mendukung
Penggantian Kebijakan Izin
Jika browser tidak mendukung pendelegasian otp-credentials melalui Permissions-Policy dan <iframe allow="otp-credentials">, <form> verifikasi yang disematkan di dalam <iframe> lintas origin akan menurun secara halus ke saran keyboard autocomplete="one-time-code" dan input numerik manual tanpa menampilkan pengecualian yang tidak tertangani.
Dukungan Inputmode dan AbortController
Atribut inputmode HTML dan AbortController (AbortSignal) didukung di seluruh browser modern dan tidak memerlukan kode polyfill atau penggantian.