SMS OTP をドメイン紐付け形式+one-time-code+WebOTP でフィッシングに強くする
パスキーへ移行するまでの間、SMS の最終行を @ドメイン #コード にし、入力欄に autocomplete="one-time-code"、Android Chrome 向けに WebOTP API を組み合わせると、ワンタップで自動入力でき、別ドメイン(フィッシングサイト)では候補が出なくなる。
何を解決するか / いつ使うか
- 2 要素認証・電話番号確認・アカウント復旧・決済確認などで SMS OTP を使っていて、すぐにはパスキーへ移れないとき。
- アプリを行き来して 6 桁を手入力する離脱と、偽サイトにコードを入力させられるリスクを同時に減らしたいとき。
手順
1. 入力欄
html
<form action="/verify-otp" method="POST">
<input type="text"
inputmode="numeric"
autocomplete="one-time-code"
pattern="\d{6}"
required>
</form>type="number"は使わない(増減ボタンが出たり先頭の 0 が消えたりする)。type="tel"も旧来のハックで不要。inputmode="numeric"で数字キーボードにする。autocomplete="one-time-code"で iOS / iPadOS / macOS の Safari がキーボードに OTP を提案する。
2. SMS 本文の最終行をドメイン紐付け形式にする
認証コードは 123456 です
@example.com #123456- 最終行に
@+呼び出し元サイトのドメイン、#+ OTP。全体は 140 文字以下。 - Safari(iOS 14 以降)はこの形式なら指定ドメイン上でだけ候補を出す。別ドメインのフィッシングサイトでは自動入力されないので、ユーザーが「いつもと違う」と気づくきっかけになる。
3. WebOTP API(Android の Chrome / Opera / Vivaldi)
js
if ('OTPCredential' in window) {
window.addEventListener('DOMContentLoaded', () => {
const input = document.querySelector('input[autocomplete="one-time-code"]');
if (!input) return;
const ac = new AbortController();
const form = input.closest('form');
form?.addEventListener('submit', () => ac.abort()); // 手入力で送信したら待受を止める
navigator.credentials.get({ otp: { transport: ['sms'] }, signal: ac.signal })
.then(otp => { input.value = otp.code; form?.submit(); })
.catch(console.log);
});
}- HTTPS 必須。ユーザーの許可を経てコードが JS に渡される。
注意点
- これは UX と「別ドメインで使わせない」補助であり、OTP 自体はフィッシング耐性がない(ユーザーが手で偽サイトに打ち込めば通る)。電話番号の再割り当てや SIM 乗っ取りのリスクも残る。本命は パスキーで「詰まない」ための運用のコツ(WebAuthn)。
- どの機能が使えなくても手入力で送信できるよう、
<form>と<input>を基本にしておく。 - コーディングエージェント向けスキル集「Modern Web Guidance」がこのベストプラクティスに対応した、と @agektmr が紹介している。
出典
- @agektmr · 2026-10-15 (英語版: @agektmr · 2026-10-15 )
- web.dev「SMS OTP form best practices」(Eiji Kitamura): web.dev