SMS OTP フォームのベスト プラクティス

SMS OTP フォームを最適化してユーザー エクスペリエンスを向上させる方法について説明します。

ユーザーの電話番号を確認する一般的な方法として、SMS で配信される OTP(ワンタイム パスワード)の提供をユーザーに依頼します。SMS OTP のユースケースはいくつかあります。

  • 2 要素認証。ユーザー名とパスワードに加えて、SMS OTP は、SMS OTP を受信したユーザーがアカウントを所有していることを示す強力なシグナルとしても使用できます。
  • 電話番号の確認。一部のサービスでは、電話番号をユーザーのメインの ID として使用します。このようなサービスでは、ユーザーは電話番号と SMS で受信した OTP を入力して本人確認を行うことができます。PIN と組み合わせて 2 要素認証を構成することもあります
  • アカウント復元。ユーザーがアカウントにアクセスできなくなった場合、アカウントを復元する方法が必要です。一般的なアカウント復元方法としては、登録済みのメールアドレスへのメールの送信や、電話番号への SMS OTP の送信があります。
  • 支払いの確認。支払いシステムでは、セキュリティ上の理由から、銀行やクレジット カード発行者が支払人に追加の認証を要求する場合があります。SMS OTP は、この目的でよく使用されます。

この投稿では、上記のユースケースで SMS OTP フォームを作成するためのベスト プラクティスについて説明します。

チェックリスト

SMS OTP で最適なユーザー エクスペリエンスを提供する手順は次のとおりです。

  • 次の要素で <input> 要素を使用します。
    • type="text"
    • inputmode="numeric"
    • autocomplete="one-time-code"
  • OTP SMS メッセージの最後の行として @BOUND_DOMAIN #OTP_CODE を使用します。
  • WebOTP API を使用します。

<input> 要素を使用する

すべてのブラウザで機能するため、<input> 要素を含むフォームを使用するのが最もおすすめの方法です。この投稿の他の候補が一部のブラウザで機能しなくても、ユーザーは OTP を手動で入力して送信できます。

<form action="/verify-otp" method="POST">
  <input type="text"
         inputmode="numeric"
         autocomplete="one-time-code"
         pattern="\d{6}"
         required>
</form>

入力フィールドでブラウザの機能を最大限に活用するためのアイデアをいくつかご紹介します。

type="text"

通常、OTP は 5 桁または 6 桁の数字であるため、入力フィールドに type="number" を使用すると、モバイル キーボードが数字のみに変更されるため、直感的に思われるかもしれません。ブラウザでは入力フィールドが複数の数値のシーケンスではなくカウント可能な数値であると想定されており、予期しない動作を引き起こす可能性があるため、この方法はおすすめしません。type="number" を使用すると、入力フィールドの横に上下のボタンが表示されます。これらのボタンを押すと、数値が増減し、先行するゼロが削除されることがあります。

代わりに type="text" を使用してください。これによってモバイル キーボードが数字のみに変換されるわけではありませんが、inputmode="numeric" を使用する次のヒントでその機能が得られるため、問題ありません。

inputmode="numeric"

モバイル キーボードを数字のみに変更するには、inputmode="numeric" を使用します。

一部のウェブサイトでは、フォーカス時にモバイル キーボードが数字のみ(*# を含む)に切り替わるため、OTP の入力フィールドに type="tel" を使用していることがあります。このハッキングは、これまで inputmode="numeric" が広くサポートされていなかったときに使用されていました。Firefox では inputmode="numeric" のサポートが開始されたため、意味的に正しくない type="tel" ハッキングを使用する必要はありません。

autocomplete="one-time-code"

autocomplete 属性を使用すると、デベロッパーはオートコンプリート アシスタンスを提供するためにブラウザが必要とする権限を指定し、そのフィールドで期待される情報のタイプをブラウザに知らせることができます。

autocomplete="one-time-code" により、フォームが開いている間にユーザーが SMS メッセージを受信すると、オペレーティング システムは SMS 内の OTP をヒューリスティックで解析し、キーボードがユーザーに入力する OTP を提案します。iOS、iPadOS、macOS の Safari 12 以降でのみ機能しますが、これらのプラットフォームで SMS OTP のエクスペリエンスを簡単に改善できるため、使用することを強くおすすめします。

「autocomplete="one-time-code"」の実例。

autocomplete="one-time-code" はユーザー エクスペリエンスを向上させますが、SMS メッセージがオリジンにバインドされたメッセージ形式に準拠していることを確認することで、さらに改善できます。

SMS テキストの書式を設定する

SMS を介して配信される送信元にバインドされたワンタイム コードの仕様に沿って、OTP を入力する際のユーザー エクスペリエンスを向上させます。

形式ルールはシンプルです。@ で始まるレシーバー ドメインと # で始まる OTP で SMS メッセージを終了します。

次に例を示します。

Your OTP is 123456

@web-otp.glitch.me #123456

OTP メッセージに標準形式を使用すると、OTP メッセージからのコードの抽出が容易になり、信頼性が向上します。OTP コードをウェブサイトに関連付けると、ユーザーをだまして悪意のあるサイトにコードを提供させるのが難しくなります。

この形式を使用すると、次のようなメリットがあります。

  • OTP はドメインにバインドされます。ユーザーが SMS メッセージで指定されたドメイン以外のドメインを使用している場合、OTP の候補は表示されません。これにより、フィッシング攻撃やアカウントの不正使用のリスクも軽減されます。
  • ブラウザは、ミステリアスで不安定なヒューリスティックに頼ることなく、OTP を確実に抽出できるようになります。

ウェブサイトで autocomplete="one-time-code" を使用している場合、iOS 14 以降を搭載した Safari では、上記のルールに沿って OTP が提案されます。

この SMS メッセージ形式は、Safari 以外のブラウザにもメリットがあります。Android 版 Chrome、Opera、Vivaldi も、WebOTP API を使用したオリジンにバインドされた 1 回限りのコードルールをサポートしていますが、autocomplete="one-time-code" はサポートしていません。

WebOTP API を使用する

WebOTP API は、SMS メッセージで受信した OTP へのアクセスを提供します。otp タイプ(OTPCredential)で navigator.credentials.get()transportsms が含まれている)を呼び出すと、ウェブサイトは、オリジンにバインドされたワンタイム コードに準拠する SMS が送信され、ユーザーによってアクセス権が付与されるのを待機します。OTP が JavaScript に渡されると、ウェブサイトはそれをフォームで使用するか、サーバーに直接 POST できます。

navigator.credentials.get({
  otp: {transport:['sms']}
})
.then(otp => input.value = otp.code);
WebOTP API の動作。

WebOTP API の使用方法について詳しくは、WebOTP API を使用してウェブ上の電話番号を確認するをご覧になるか、次のスニペットをコピーして貼り付けてください。(<form> 要素に action 属性と method 属性が正しく設定されていることを確認します)。

// Feature detection
if ('OTPCredential' in window) {
  window.addEventListener('DOMContentLoaded', e => {
    const input = document.querySelector('input[autocomplete="one-time-code"]');
    if (!input) return;
    // Cancel the WebOTP API if the form is submitted manually.
    const ac = new AbortController();
    const form = input.closest('form');
    if (form) {
      form.addEventListener('submit', e => {
        // Cancel the WebOTP API.
        ac.abort();
      });
    }
    // Invoke the WebOTP API
    navigator.credentials.get({
      otp: { transport:['sms'] },
      signal: ac.signal
    }).then(otp => {
      input.value = otp.code;
      // Automatically submit the form when an OTP is obtained.
      if (form) form.submit();
    }).catch(err => {
      console.log(err);
    });
  });
}

写真撮影: Jason LeungUnsplash より