SMS OTP form doğrulaması

SMS ile tek kullanımlık şifre kodu (OTP) göndermek; telefon numarası doğrulama, hesap kurtarma, ek kimlik doğrulama ve ödeme onayı için yaygın olarak kullanılan bir yöntemdir. Çok haneli bir kodu ezberlemek veya kopyalayıp yapıştırmak için tarayıcı ile SMS mesajlaşma uygulaması arasında geçiş yapmak kullanıcı deneyimini olumsuz etkiler, ayrılma oranlarını artırır ve kullanıcıları sahte alan adlarında kimlik avına karşı savunmasız bırakır.

Anlamsal <form> işaretlemesi, <input autocomplete="one-time-code" inputmode="numeric">, kaynağa bağlı SMS biçimlendirmesi (@domain #code) ve WebOTP API'yi (OTPCredential) birleştirmek, katmanlı ve progresif geliştirilmiş bir doğrulama akışı oluşturur:

  1. Otomatik ayıklama ve gönderme: WebOTP API'yi destekleyen tarayıcılar, SMS'teki @domain #code satırına göre kaynağı doğrular, <input> alanını doldurur ve tek bir dokunuşla doğrulamayı tetikler.
  2. Kaynağa bağlı klavye otomatik doldurma: autocomplete="one-time-code" standardını destekleyen tarayıcılar ve işletim sistemleri (ör. iOS, iPadOS ve macOS'te Safari), @domain #code mesajını ayrıştırır ve yalnızca alan adı eşleştiğinde klavye otomatik doldurma çubuğunda kodu önerir.
  3. Sorunsuz manuel giriş: Başka bir cihazdan kod giren kullanıcılar, <input type="number">'nin kullanılabilirlik tehlikeleri olmadan inputmode="numeric" üzerinden sayısal bir sanal tuş takımı alır.

Güvenlikle ilgili olarak göz önünde bulundurulması gerekenler

SMS OTP, telefon numarasının size ait olduğunu doğrular ve kolay bir ek doğrulama adımı sunar. Ancak SMS aktarımı ve telefon numaraları; SIM takası, numara geri dönüşümü ve müdahaleye maruz kalabilir. Tarayıcı ve işletim sisteminin, kodu önermeden veya göndermeden önce hedef alanın eşleştiğini doğrulaması için istemci tarafı otomatik doldurma ve WebOTP API'yi her zaman kaynağa bağlı @domain #code SMS biçimiyle eşleştirin. Birincil kimlik avına dayanıklı kimlik doğrulama için SMS tabanlı kodlar yerine geçiş anahtarlarını tercih edin (Geçiş Anahtarları Oryantasyonu'na bakın).

Anlamsal form işaretlemesi

Doğrulama kontrolünü method="POST" ve açık bir <button type="submit"> içeren anlamsal bir <form> öğesiyle sarmalayın. Rakamları birden fazla kutuya bölmek yerine her zaman tek bir <input> öğesi kullanın.

  • type="text"'i inputmode="numeric" ile kullanın: OTP kodları için hiçbir zaman type="number" kullanmayın. Tek kullanımlık şifre kodu, sayılabilir bir miktar değil, başında sıfır (ör. 012345) bulunabilen bir tanımlayıcı dizedir. type="number" kullanıldığında artırma/azaltma düğmeleri eklenir, baştaki sıfırlar kaldırılabilir ve kaydırma tekerleğiyle yanlışlıkla değer değişiklikleri yapılabilir. Ayrıca, tam telefon numaraları için kullanılan eski bir geçici çözüm olan type="tel" özelliğini kullanmaktan kaçının. Mobil cihazlarda yalnızca rakamlardan oluşan bir sanal klavye göstermek için type="text" ile inputmode="numeric" öğesini eşleyin.
  • autocomplete="one-time-code" ekleyin: Tarayıcılara ve işletim sistemlerine, <input> odaklandığında otomatik doldurma çubuğunda gelen SMS doğrulama kodlarını önermeleri talimatını verir.
  • Talimat ipuçlarını <input> öğesinin üstüne yerleştirin: for ve id kullanarak görünür bir <label> ilişkilendirin ve tüm biçim talimatlarını (aria-describedby aracılığıyla bağlantılı) görsel olarak <input> öğesinin üstüne yerleştirin. Böylece otomatik doldurma popover'ları ve mobil sanal klavyeler talimatları hiçbir zaman kapatmaz.
  • Rakamları birden fazla <input> öğesine bölmeyin: Çok kutulu OTP kalıpları (altı ayrı maxlength="1" girişi), birçok tarayıcıda yerel autocomplete="one-time-code" otomatik doldurma özelliğini bozar, kopyalama ve yapıştırmayı engeller ve ekran okuyucu gezinmesini zorlaştırır. Segmentlere ayrılmış bir görsel tasarım gerekiyorsa letter-spacing ve sabit boşluklu yazı tipiyle tek bir <input> öğesini biçimlendirin.
<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;
}

Kaynağa bağlı SMS mesajı biçimi

Hem WebOTP API'nin hem de Safari'nin alana bağlı autocomplete="one-time-code" otomatik doldurma özelliğinin, kodun web sitenize ait olduğunu doğrulayabilmesi için arka uç SMS hizmetinizin, kısa mesajın son satırını, kaynak alanın önüne @ ve OTP'nin önüne # ekleyerek biçimlendirmesi gerekir:

Your verification code is 123456.

@example.com #123456

Giden SMS mesajında aşağıdaki biçimlendirme kurallarını uygulayın:

  • Önce kullanıcıların okuyabileceği metni ekleyin: SMS'e her zaman kodu içeren, kullanıcıların okuyabileceği net bir cümleyle başlayın. Böylece, kodu başka bir cihaza manuel olarak yazan kullanıcılar kolayca okuyabilir.
  • @<domain> #<otp> karakterini en son satıra yerleştirin: @ karakteri, son satırın ilk karakteri olmalı ve ardından gelen satır veya imza metni olmamalıdır.
  • Yalnızca ana makine alan adını kullanın: @domain jetonuna URL şeması (https:// veya http://), bağlantı noktası (:8080) ya da yol (/verify) eklemeyin.
  • Jetonları tek bir boşlukla ayırın: @<domain> ve #<otp> arasına tam olarak bir boşluk bırakın ve alan adının içinde veya # ile kod arasında hiçbir zaman boşluk bırakmayın.

WebOTP API'yi doğrulama akışınıza entegre etme

WebOTP API (OTPCredential) için güvenli bir bağlam (üretimde https:// veya yerel geliştirme sırasında http://localhost) gerekir. DOM'u sorgulayan ve form.submit() yöntemini çağıran bağlantısız bir DOMContentLoaded komut dosyası yapıştırmak yerine, navigator.credentials.get() yöntemini doğrudan doğrulama kullanıcı arayüzünüzün yaşam döngüsüne ve gönderim işleyicisine entegre edin:

  • İstek yaşam döngüsünü etkin OTP adımına bağlayın: Doğrulama girişi oluşturulup etkinleştirildiğinde OTP'yi dinlemeye başlayın (örneğin, kullanıcı bir SMS kodu istedikten sonra OTP adımı yüklendiğinde ve kullanıcı "Kodu yeniden gönder" düğmesini her tıkladığında).
  • Tüm çıkış yollarında AbortController yönetme: navigator.credentials.get() öğesine bir AbortSignal (signal: ac.signal) iletin. Aşağıdaki durumlarda ac.abort() numarasını arayın:
    • Kullanıcı, formu manuel olarak gönderir (veya autocomplete="one-time-code" aracılığıyla doldurulan bir kodu gönderir).
    • Yeni bir SMS kodu istendiğinde ("Kodu yeniden gönder") yeni bir denetleyici oluşturulmadan önce önceki denetleyici iptal edilir.
    • Doğrulama bileşeni, iletişim kutusu veya rota kaldırılır.
  • Uygulamanın gönderim işleyicisini (form.requestSubmit()) tetikleyin: navigator.credentials.get(), otp ile çözümlendiğinde otpInput.value = otp.code değerini ayarlayın ve form.requestSubmit() işlevini çağırın (veya doğrulama işlevinizi doğrudan çağırın). form.submit(), submit etkinlik işleyicileri (event.preventDefault() / fetch() işleyicileri) atladığı ve yerel kısıtlama doğrulamasını atladığı için form.submit()'yi ÇAĞIRMAYIN.
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();

Çapraz kaynak iframe doğrulaması

Çapraz kaynak <iframe> içinde SMS OTP alma varsayılan olarak kısıtlanmıştır ve öncelikle yerleşik ödeme onayı akışları için kullanılır (ör. shop.example, bank.example tarafından gönderilen bir sorgulamayı yerleştirdiğinde 3D Secure doğrulaması). Çapraz kaynak <iframe> içinde WebOTP'yi desteklemek için üç koordineli yapılandırma gerekir:

  1. SMS'in son satırında her iki kaynağı da bağlayın: Önce üst düzey yerleştirme alanını (@shop.example), ardından #<otp>, bir boşluk ve yerleştirilmiş iFrame alanını (@bank.example) listeleyin:

    Your verification code is 123456.
    
    @shop.example #123456 @bank.example
    
  2. Permissions-Policy HTTP yanıt başlığını gönderin: Üst dokümanın HTTP başlıklarında otp-credentials'yi çapraz kaynak iframe kaynağına devredin:

    Permissions-Policy: otp-credentials=(self "https://bank.example")
    
  3. <iframe> öğesine allow="otp-credentials" ekleyin: Yerleştirme sayfasındaki <iframe> etiketinde izin politikasını açıkça verin ve ekran okuyucular için açıklayıcı bir title özelliği ekleyin:

<iframe
  src="https://bank.example/verify"
  allow="otp-credentials"
  title="Payment verification">
</iframe>

https://bank.example tarafından sunulan <iframe> dokümanında, yukarıda gösterilen <form> işaretleme ve navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal }) doğrulama yaşam döngüsünü çalıştırın.

Yedek stratejiler

WebOTP API yedek seçeneği

Hedef tarayıcılarınız WebOTP API'yi (OTPCredential) desteklemiyorsa üçüncü taraf JavaScript polyfill yüklemeyin. Gelen SMS mesajlarını okumak için işletim sistemi düzeyinde entegrasyon gerekir. Bunun yerine, <input> işaretlemesinde yerleşik olarak bulunan üç katmanlı progresif geliştirmeyi kullanın:

  • Progresif geliştirme deneyimi: navigator.credentials.get({ otp: { transport: ['sms'] } }) çağrısını if ('OTPCredential' in window) ile koruyun. OTPCredential kullanılamadığında:
    1. autocomplete="one-time-code" özelliğini destekleyen tarayıcılar (ör. iOS 14 ve sonraki sürümlerde, iPadOS'te ve iCloud SMS geçişi ile macOS'te Safari), @domain #code SMS'ini ayrıştırır ve <input> odaklandığında kodu sistem klavyesi otomatik doldurma çubuğunda gösterir.
    2. Diğer tüm tarayıcılar, tek bir <input type="text" inputmode="numeric" pattern="\d{6}" maxlength="6" required> içinde manuel girişe geri döner, mobil cihazlarda sayısal tuş takımı gösterir ve <button type="submit"> aracılığıyla gönderir.

İzin Politikası yedeklemesi

Tarayıcı, Permissions-Policy ve <iframe allow="otp-credentials"> aracılığıyla otp-credentials yetkilendirmesini desteklemiyorsa çapraz kaynak <iframe> içindeki yerleşik doğrulama <form>, işlenmeyen istisnalar oluşturmadan autocomplete="one-time-code" klavye önerilerine ve manuel sayısal girişe sorunsuz bir şekilde geçiş yapar.

Inputmode ve AbortController desteği

Hem HTML inputmode özelliği hem de AbortController (AbortSignal) modern tarayıcılarda desteklenir ve polyfill veya yedek kod gerektirmez.