1. 導入
このセクションは規範的ではありません。
多くのウェブサイトは認証フローの一部として資格情報(例:電話番号やメールアドレス)の検証を必要とします。現在は、これらの通信チャネルにワンタイムパスワード(OTP)を送信し、所有権の証明として利用しています。ワンタイムパスワードは手動でユーザーがウェブアプリに渡す必要があり(通常はコピー&ペースト)、これは面倒で誤りも発生しやすいです。
これは、ウェブサイトがOTPを要求できるクライアントサイドのJavaScript APIと、ブラウザと協調して利用できる一連のトランスポート固有の規約(まずはSMSから開始し、他の手段にも拡張可能)を提案するものです。
1.1. クライアントサイドAPI
この提案では、ウェブサイトは特定のトランスポート(例:SMS)から受信するOTPを取得するためにブラウザAPIを呼び出すことができます。
ブラウザはSMSの受信と呼び出し元ウェブサイトへの受け渡しを仲介し(通常はユーザーの同意を求める)、APIは非同期でPromiseを返します。
1.2. サーバーサイドAPI
クライアントサイドAPIが呼び出されると、ウェブサイトのサーバーは要求されたトランスポート手段を使ってOTPをクライアントに送信できます。各トランスポート手段ごとに、安全かつプログラム的にOTPが配信されることを保証するサーバーサイドの規約が設定されます。
SMSの場合、サーバーはオリジンバウンドワンタイムコードメッセージをクライアントに送信する必要があります。[sms-one-time-codes]
次のオリジンバウンドワンタイムコードメッセージでは、ホストは"example.com"
、コードは"123456"
、説明文は"Your authentication code is 123456.\n"
です。
"Your authentication code is 123456. @example.com #123456"
1.3. 機能検出
すべてのユーザーエージェントが必ずしも同じタイミングでWebOTP APIを実装するわけではないため、ウェブサイトはAPIが利用可能かどうかを検出する仕組みが必要です。
ウェブサイトはOTPCredentialグローバルインターフェースの有無を確認できます:
1.4. Webコンポーネント
主にOTP認証は以下に依存しています:
-
クライアントサイドでのinput、フォーム、コピー&ペースト
-
サーバーサイドでのSMS送信のためのサードパーティフレームワーク
これらのフレームワークの一部は、顧客の既存コードの導入を容易にするために、このAPIの宣言的バージョンを開発することが期待されます。
< script src = "sms-sdk.js" ></ script > < form > < input is = "one-time-code" required /> < input type = "submit" /> </ form >
また、フレームワークがWebコンポーネントを使ってどのように実装できるかの例です:
customElements. define( "one-time-code" , class extends HTMLInputElement{ connectedCallback() { this . receive(); } async receive() { let { code, type} = await navigator. credentials. get({ otp: { transport: [ "sms" ] } }); this . value= otp; this . form. submit(); } }, { extends : "input" });
1.5. Abort API
多くのモダンなウェブサイトはクライアントサイドでナビゲーションを処理します。そのため、ユーザーがOTPフローから他のフローへ移動した場合、リクエストをキャンセルして、不要な権限プロンプトでユーザーが煩わされないようにする必要があります。
そのために、リクエストを中断するためのabort controllerを渡すことができます:
const abort= new AbortController(); setTimeout(() => { // 2分後に中断 abort. abort(); }, 2 * 60 * 1000 ); let { code, type} = await navigator. credentials. get({ signal: abort. signal, otp: { transport: [ "sms" ] } });
2. クライアントサイドAPI
ウェブサイトは
navigator.credentials.get({otp:..., ...})
を呼び出してOTPを取得します。
navigator.credentials.get()
のアルゴリズムは、Request a Credential
抽象操作において、
Credential
を継承するすべてのインターフェースを調べます。
その操作内で、OTPCredential
を見つけ、これは Credential
を継承しています。
そして
OTPCredential.
を呼び出して、資格情報のうち
ユーザー操作なしで利用可能なものを収集します。もしそのような資格情報がちょうど1つ見つからなかった場合、
[[CollectFromCredentialStore]]()
OTPCredential.
を呼び出して、ユーザーに資格情報のソースを選択させリクエストを完了させます。
[[DiscoverFromExternalSource]]()
この仕様はOTP 認可ジェスチャ の作成を要求するため、
OTPCredential.
内部メソッド は [[CollectFromCredentialStore]]()
Credential.[[CollectFromCredentialStore]]()
のデフォルト動作(空集合を返す)を継承します。
したがって
OTPCredential.
がOTPを提供する責任を持ちます。
[[DiscoverFromExternalSource]]()
2.1. OTPCredentialインターフェース
OTPCredential
インターフェースは Credential
を拡張し、
新しいワンタイムパスワードが取得された際に呼び出し元へ返される属性を含みます。
OTPCredential
の
インターフェースオブジェクトは Credential
の
[[CollectFromCredentialStore]](origin, options, sameOriginWithAncestors)
の実装を継承し、
独自実装の [[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
を定義します。
[Exposed =Window ,SecureContext ]interface :
OTPCredential Credential {readonly attribute DOMString code ; };
id
-
この属性は
Credential
から継承されます。 [[type]]
-
OTPCredential
の インターフェースオブジェクトにおける[[type]]
内部スロットの値は "otp
" です。 code
, 型は DOMString、readonly-
取得されたワンタイムパスワードです。
2.1.1.
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
メソッド
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
このメソッドは
navigator.credentials.get({otp:..., ...})
が呼び出されるたびに呼び出され、OTPが要求された際(すなわち
options.
が渡された場合)にOTPを返す役割を持ちます。
otp
この 内部メソッド は3つの引数を受け取ります:
origin
-
この引数は、呼び出し元
get()
実装で決定される 関連設定オブジェクト の origin です。すなわちCredentialsContainer
の Request aCredential
抽象操作です。 options
-
この引数は
CredentialRequestOptions
オブジェクトであり、options.
メンバーが取得したいOTPの属性を指定するotp
OTPCredentialRequestOptions
オブジェクトを含みます。 sameOriginWithAncestors
-
この引数は、呼び出し元の 環境設定オブジェクト が 祖先と同一オリジンである場合に限り
true
となる真偽値です。呼び出し元がクロスオリジンの場合はfalse
です。注: この 内部メソッド の呼び出しは permissions policy により許可されていることを示します。これは [CREDENTIAL-MANAGEMENT-1] のレベルで評価されます。 § 2.5 Permissions Policy integration 参照。
注: このアルゴリズムは同期的です:
Promise
のresolve/rejectは navigator.credentials.get()
によって処理されます。
このメソッドが呼び出された際、ユーザーエージェントは次のアルゴリズムを実行しなければなりません:
-
options を
options.
の値とする。otp
-
callerOrigin を
origin
とする。 callerOrigin が 不透明オリジンの場合は、名前が "NotAllowedError
" のDOMException
を返し、このアルゴリズムを終了する。 -
effectiveDomain を callerOrigin の 有効ドメイン とする。 有効ドメイン が 有効なドメインでない場合は、名前が "
SecurityError
" のDOMException
を返し、このアルゴリズムを終了する。注: 有効ドメインは ホストに解決される場合があり、ドメイン、IPv4アドレス、 IPv6アドレス、不透明ホスト、 空ホストなど様々な表現が可能です。 ここでは ドメイン形式のみ許可されます。これは簡素化のため、またPKIベースのセキュリティで 直接IPアドレス識別を使用することの諸問題を考慮したものです。
-
options.
が 存在し、その aborted flag がsignal
true
の場合は、 名前が "AbortError
" のDOMException
を返し、このアルゴリズムを終了する。 -
TODO(goto): トランスポートアルゴリズムとの接続方法を検討。
上記の処理中、ユーザーエージェントはユーザーがオリジンとOTPを共有するプロセスをガイドするUIを表示するべきです。
2.2. CredentialRequestOptions
OTPを navigator.credentials.get()
で取得できるようにするため、本書では CredentialRequestOptions
辞書を次のように拡張します:
partial dictionary CredentialRequestOptions {OTPCredentialRequestOptions otp ; };
otp
, 型は OTPCredentialRequestOptions-
このオプションのメンバーはWebOTPリクエストに使用されます。
2.3.
OTPCredentialRequestOptions
OTPCredentialRequestOptions
辞書は、
navigator.credentials.get()
に
OTPを取得するために必要なデータを提供します。
dictionary {
OTPCredentialRequestOptions sequence <OTPCredentialTransportType >transport = []; };
transport
, 型は sequence<OTPCredentialTransportType>、デフォルト値は[]
-
このオプションのメンバーは、サーバーがOTPをどのように受け取るかのヒントを含みます。 値は
OTPCredentialTransportType
のメンバーであるべきですが、クライアントプラットフォームは未知の値を無視しなければなりません。
2.4. OTPCredentialTransportType
enum {
OTPCredentialTransportType "sms" , };
sms
-
OTPがSMSで届くことが期待されることを示します。
2.5. Permissions Policyとの統合
この仕様は、feature-identifierトークン
"otp-credentials
"
で識別される ポリシー制御機能 を1つ定義します。
その デフォルト許可リスト は 'self
' です。[Permissions-Policy]
Document
の permissions policy は、その 文書内のコンテンツが WebOTP APIを正常に使用できるか を決定します。すなわち
navigator.credentials.get({otp: { transport: ["sms"]}})
の呼び出しが可能かどうかです。
いずれかの文書で無効化されている場合、その文書内の全コンテンツはこれらのメソッドを 使用できません。使用しようとすると エラーが返されます。
2.6. iframe
要素でWebOTPを利用する
WebOTP APIは、オリジンが一致していれば内部フレームで利用可能ですが、クロスオリジン
iframe
ではデフォルトで無効となっています。
このデフォルトポリシーを上書きし、クロスオリジン
iframe
でも WebOTP API の
[[DiscoverFromExternalSource]](origin, options, sameOriginWithAncestors)
メソッドを使用可能にするには、
allow
属性を
iframe
要素に指定し、さらに
otp-credentials
feature-identifierトークンを
allow
属性値に含めます。
WebOTP APIを埋め込み環境で利用するRelying Partiesは、§ 4.4 埋め込み利用時の可視性に関する考慮事項で説明されている UIの改ざんとその対策も確認してください。
3. トランスポート
OTPを受信可能にするため、さまざまなトランスポート手段が想定されます。代表的なものはSMS、メール、ハードウェアデバイスです。
それぞれのトランスポートには、OTPをブラウザへ提供するための独自の規約が必要です。
このドラフトでは、APIの表面は任意の数のトランスポートに拡張可能な設計となっています。
3.1. SMS
OTPの最も一般的なトランスポート手段の一つがSMSメッセージです。これにより開発者は電話番号の検証が可能となります。OTPは通常SMSメッセージに埋め込まれて送信され、ユーザーがコピー&ペーストして使用します。
[sms-one-time-codes]は、OTPをSMS経由で送信しオリジンと紐付けるための オリジンバウンドワンタイムコードメッセージの形式を定義しています。
4. セキュリティ
セキュリティの観点から、このAPIには2つの考慮点があります:
-
改ざん:メッセージの変更に基づく攻撃の防止。
4.1. 可用性
このAPIが利用可能なのは以下の場合のみです:
-
https(または開発用途のlocalhost)
このAPIはhttpsまたはlocalhost(開発用)でのみ利用可能です。信頼できるURLの概念は完全には採用していません。なぜならその概念は、私たちが想定しないスキーム(例:data://123)も含むためです。(初期の直感としては、(a) httpsとlocalhostでほとんどのケースをカバーできること、(b) SMSを送信する公開側のエンティティがユーザーに明確である必要があること、が挙げられます。)
4.2. アドレス指定
各トランスポートは、ブラウザがOTPを適切なオリジンにルーティングできるだけの情報を保証する責任があります。
たとえば、オリジンバウンドワンタイムコードメッセージは、OTPを使用できるオリジンを明示的に識別します。
アドレス指定方式は、エージェントによって強制され、適切にルーティングされることが保証されなければなりません。
4.3. 改ざん
このAPIが返すOTPが改ざんされていないという暗号的な保証は組み込まれていません。たとえば、攻撃者がユーザーの電話に任意のオリジンで オリジンバウンドワンタイムコードメッセージ を送り、エージェントがそれをリクエスト元へそのまま返してしまう可能性があります。
呼び出し元が責任を持って、以下を行う必要があります:
-
受信したOTPが有効なものかを検証するためのチェックを実装する。例:
-
既知のフォーマット(例:英数字のみ)に従って慎重にパースすること
-
送信したOTPをサーバーサイドのデータベースで管理・照合すること
-
-
無効なOTPが受信された場合は適切にフォールバックする(例:再リクエストするなど)。
4.4. 埋め込み利用時の可視性に関する考慮事項
WebOTPを埋め込み環境で単純に利用(例:iframe
内で
§ 2.6 iframe要素内でのWebOTP利用 を参照)すると、ユーザーが UIレドレッシング攻撃(別名「クリックジャッキング」)の被害に遭う可能性があります。これは攻撃者が Relying Party
の本来意図したUIの上に自身のUIを重ね、ユーザーに意図しない操作をさせる手法です。例として、こうした技法により攻撃者がユーザーに商品の購入や送金などをさせる可能性があります。
5. プライバシー
プライバシーの観点で最も重要なのは、ユーザーとウェブサイト間で情報交換が合意に基づいて行われるようユーザーエージェントが強制することです。
具体的には、このAPIにより、ユーザーの個人識別可能な属性(例:メールアドレスや電話番号)のプログラムによる検証が可能となります。
最も頻繁に挙げられる攻撃ベクトルは、ターゲット型攻撃です。すなわち、ウェブサイトが全ユーザーの中から特定のユーザーを探そうとするケースです。この攻撃では、放置するとウェブサイトがこのAPIを使い、全ユーザーや一部(信頼度による)に オリジンバウンドワンタイムコードメッセージ を送信し、誰かが受信したタイミングを検出することで 特定の電話番号を持つユーザーを探すことができます。
重要なのは、このAPIが個人情報の取得を助けるものではなく、その検証を支援するものだという点です。つまり、このAPIはユーザーが特定の電話番号を所有しているか検証するためのものであり、電話番号そのものを取得するものではありません(ウェブサイトがすでにアクセス権を持っていることが前提です)。
それでも、これら属性の所有の検証はユーザーの追加情報となるため、ユーザーエージェントは責任を持って管理すべきです。通常はOTPをウェブサイトへ返す前に権限プロンプトでユーザーの同意を得るべきです。
6. 謝辞
Steven Soneff, Ayu Ishii, Reilly Grant, Eiji Kitamura, Alex Russell, Owen Campbell-Moore, Joshua Bell, Ricky Mondello, Mike West の皆様に、この提案の作成にご協力いただき感謝申し上げます。
特にTab Atkins, Jr.氏には、仕様作成ツールBikeshedの開発・保守、そして執筆に関するアドバイスをいただき感謝いたします。