既存のパスワード ユーザーにも対応しつつ、パスキーを活用するログイン エクスペリエンスを作成します。
パスキーはパスワードに代わるもので、ウェブ上のユーザー アカウントの安全性、シンプルさ、使いやすさを向上させます。ただし、パスワード ベースの認証からパスキー ベースの認証に移行すると、ユーザー エクスペリエンスが複雑になる可能性があります。フォームの自動入力を使用してパスキーを提案すると、統一感のあるエクスペリエンスを構築できます。
パスキーによるログインにフォームの自動入力を使用する理由
パスキーを使用すると、ユーザーは指紋認証、顔認証、デバイスの PIN を使用してウェブサイトにログインできます。
理想的には、パスワードを使用するユーザーが存在せず、認証フローがシングル サインオン ボタンのようにシンプルである必要があります。ユーザーがボタンをタップすると、アカウント選択ダイアログがポップアップ表示されます。ユーザーはアカウントを選択し、画面のロックを解除して確認とログインを行えます。
ただし、パスワードからパスキー ベースの認証への移行は難しい場合があります。ユーザーがパスキーに切り替えても、パスワードを使用するユーザーは引き続き存在するため、ウェブサイトは両方のタイプのユーザーに対応する必要があります。ユーザーがパスキーに切り替えたサイトを覚えておくことは期待できません。そのため、どの方法を使用するか事前に選択するようユーザーに求めるのは UX が悪いことになります。
パスキーも新しいテクノロジーです。ウェブサイトでは、それらを説明してユーザーが快適に使用できるようにすることが課題となります。どちらの問題も、使い慣れたユーザー エクスペリエンスでパスワードを自動入力できます。
条件付き UI
パスキーとパスワード ユーザーの両方にとって効率的なユーザー エクスペリエンスを実現するには、自動入力の候補にパスキーを含めます。これは条件付き UI と呼ばれ、WebAuthn 標準の一部です。
ユーザーがユーザー名の入力フィールドをタップすると、自動入力候補ダイアログがポップアップ表示され、保存されているパスキーとパスワードの自動入力候補がハイライト表示されます。ユーザーはアカウントを選択し、デバイスの画面ロックを使用してログインできます。
これにより、ユーザーは何も変更していないかのように既存のフォームを使用してウェブサイトにログインできますが、パスキーのセキュリティ上のメリットが得られます。
仕組み
パスキーで認証するには、WebAuthn API を使用します。
パスキー認証フローの 4 つのコンポーネントは、次のとおりです。
- バックエンド: パスキーに関する公開鍵やその他のメタデータを保存するアカウント データベースを保持するバックエンド サーバー。
- フロントエンド: ブラウザと通信し、バックエンドにフェッチ リクエストを送信するフロントエンドです。
- ブラウザ: JavaScript を実行しているユーザーのブラウザ。
- 認証システム: パスキーを作成して保存するユーザーの認証システム。これは、ブラウザと同じデバイス(Windows Hello を使用している場合など)またはスマートフォンなどの別のデバイスに配置できます。
- ユーザーがフロントエンドにアクセスするとすぐに、パスキーで認証するためのチャレンジをバックエンドにリクエストし、
navigator.credentials.get()
を呼び出してパスキーによる認証を開始します。Promise
が返されます。 - ユーザーがログイン フィールドにカーソルを合わせると、パスキーを含むパスワード自動入力ダイアログがブラウザに表示されます。ユーザーがパスキーを選択すると、認証ダイアログが表示されます。
- ユーザーがデバイスの画面ロックを使用して本人確認を行うと、プロミスが解決され、公開鍵認証情報がフロントエンドに返されます。
- フロントエンドは公開鍵認証情報をバックエンドに送信します。バックエンドは、データベース内の一致したアカウントの公開鍵と署名を照らし合わせて検証します。成功すると、ユーザーはログインされます。
フォームの自動入力でパスキーを使用して認証する
ユーザーがログインしようとしたときに、条件付きの WebAuthn get
呼び出しを行い、自動入力候補にパスキーが含まれる可能性があることを示します。WebAuthn の navigator.credentials.get()
API の条件付き呼び出しでは、UI は表示されず、ユーザーがログインに使用するアカウントを自動入力候補から選択するまで保留状態になります。ユーザーがパスキーを選択すると、ブラウザはログイン フォームに入力するのではなく、認証情報を使用して Promise を解決します。この場合、ユーザーのログインはページの役割です。
フォームの入力フィールドにアノテーションを付ける
必要に応じて、ユーザー名 input
フィールドに autocomplete
属性を追加します。username
と webauthn
をトークンとして追加して、パスキーを提案できるようにします。
<input type="text" name="username" autocomplete="username webauthn" ...>
特徴検出
条件付き WebAuthn API 呼び出しを呼び出す前に、次の点を確認してください。
- ブラウザが
PublicKeyCredential
で WebAuthn をサポートしている。
- ブラウザが
PublicKeyCredenital.isConditionalMediationAvailable()
で WebAuthn 条件付き UI をサポートしている。
// Availability of `window.PublicKeyCredential` means WebAuthn is usable.
if (window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable) {
// Check if conditional mediation is available.
const isCMA = await PublicKeyCredential.isConditionalMediationAvailable();
if (isCMA) {
// Call WebAuthn authentication
}
}
RP サーバーからチャレンジを取得する
navigator.credentials.get()
の呼び出しに必要なチャレンジを RP サーバーから取得します。
challenge
: サーバーが生成した、ArrayBuffer 内のチャレンジ。これはリプレイ攻撃の防止に必要です。ログインを試みるたびに新しい本人確認情報を生成し、一定時間経過後またはログイン試行の検証に失敗した後は、本人確認を無視するようにします。CSRF トークンのようなものと考えてください。allowCredentials
: この認証で使用できる認証情報の配列。空の配列を渡して、ブラウザに表示されるリストからユーザーが使用可能なパスキーを選択できるようにします。userVerification
: デバイスの画面ロックを使用するユーザー確認が"required"
、"preferred"
、"discouraged"
のいずれであるかを示します。デフォルトは"preferred"
です。つまり、認証システムはユーザー確認をスキップする場合があります。"preferred"
に設定するか、プロパティを省略します。
conditional
フラグを使用して WebAuthn API を呼び出してユーザーを認証する
navigator.credentials.get()
を呼び出して、ユーザー認証の待機を開始します。
// To abort a WebAuthn call, instantiate an `AbortController`.
const abortController = new AbortController();
const publicKeyCredentialRequestOptions = {
// Server generated challenge
challenge: ****,
// The same RP ID as used during registration
rpId: 'example.com',
};
const credential = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions,
signal: abortController.signal,
// Specify 'conditional' to activate conditional UI
mediation: 'conditional'
});
rpId
: RP ID はドメインであり、ウェブサイトはドメインまたは登録可能なサフィックスを指定できます。この値は、パスキーの作成時に使用された rp.id と一致する必要があります。
リクエストを条件付きにするには、mediation: 'conditional'
を指定してください。
返された公開鍵認証情報を RP サーバーに送信する
ユーザーがアカウントを選択し、デバイスの画面ロックを使用して同意すると、Promise が解決され、PublicKeyCredential
オブジェクトが RP フロントエンドに返されます。
プロミスが拒否される理由はいくつかあります。Error
オブジェクトの name
プロパティに応じて、エラーを適切に処理する必要があります。
NotAllowedError
: ユーザーがオペレーションをキャンセルしました。- その他の例外: 予期しない事象が発生しました。ブラウザにエラー ダイアログが表示されます。
公開鍵認証情報オブジェクトには、次のプロパティが含まれています。
id
: 認証済みパスキー認証情報の base64url でエンコードされた ID。rawId
: 認証情報 ID の ArrayBuffer バージョン。response.clientDataJSON
: クライアント データの ArrayBuffer。このフィールドには、RP サーバーで検証する必要があるチャレンジやオリジンなどの情報が含まれます。response.authenticatorData
: 認証システムデータの ArrayBuffer。このフィールドには、RP ID などの情報が含まれます。response.signature
: 署名の ArrayBuffer。この値は、認証情報の核となる情報であり、サーバーで検証する必要があります。response.userHandle
: 作成時に設定されたユーザー ID を含む ArrayBuffer。サーバーが使用する ID 値をサーバー側で選択する必要がある場合、またはバックエンドで認証情報 ID のインデックス作成が行われないようにしたい場合は、認証情報 ID の代わりにこの値を使用できます。authenticatorAttachment
: この認証情報がローカル デバイスから取得された場合は、platform
を返します。それ以外の場合は、cross-platform
(特にユーザーがスマートフォンを使用してログインした場合)。ユーザーがスマートフォンを使用してログインする必要がある場合は、ローカル デバイスでパスキーを作成するようユーザーに促すことを検討します。type
: このフィールドは常に"public-key"
に設定されます。
ライブラリを使用して RP サーバーで公開鍵認証情報オブジェクトを処理する場合は、base64url で部分的にエンコードした後、オブジェクト全体をサーバーに送信することをおすすめします。
署名の検証
サーバーで公開鍵認証情報を受け取ったら、FIDO ライブラリに渡してオブジェクトを処理します。
id
プロパティを使用して、一致する認証情報 ID を検索します(ユーザー アカウントを特定する必要がある場合は、認証情報の作成時に指定した user.id
である userHandle
プロパティを使用します)。認証情報の signature
が保存されている公開鍵で検証できるかどうかを確認します。そのためには、独自のコードを記述するのではなく、サーバーサイド ライブラリまたはソリューションを使用することをおすすめします。オープンソース ライブラリは、awesome-webauth GitHub リポジトリで確認できます。
一致する公開鍵で認証情報が検証されたら、ユーザーをログインさせます。
詳しい手順については、サーバーサイド パスキー認証をご覧ください。