既存のパスワード ユーザーに対応しながら、パスキーを利用するログイン エクスペリエンスを作成します。
パスキーはパスワードに代わるもので、ウェブ上のユーザー アカウントを安全、シンプル、使いやすくします。ただし、パスワード ベースからパスキー ベースの認証に移行すると、ユーザー エクスペリエンスが複雑になる可能性があります。フォームの自動入力を使用してパスキーを提案すると、統一感のあるエクスペリエンスを構築できます。
パスキーによるログインにフォームの自動入力を使用する理由
パスキーを使用すると、ユーザーは指紋認証、顔認証、デバイスの PIN のみでウェブサイトにログインできます。
パスワード ユーザーは存在せず、認証フローはシングル ログインボタンと同じくらいシンプルにするのが理想的です。ユーザーがボタンをタップすると、アカウント選択ダイアログがポップアップ表示されます。ユーザーはアカウントを選択し、画面をロック解除して確認とログインを行うことができます。
ただし、パスワードからパスキー ベースの認証への移行は困難な場合があります。ユーザーがパスキーに切り替えた後も、パスワードを使用するユーザーが存在し、ウェブサイトは両方のタイプのユーザーに対応する必要があります。ユーザー自身がどのサイトをパスキーに切り替えたかを覚えておく必要はないので、事前にどの方法を使用するかの選択をユーザーに求めるのは、不十分です。
パスキーは新しい技術でもあります。使い方をわかりやすく説明し、ユーザーが快適に使いこなせるようにするのは、ウェブサイトにとって難しい課題です。どちらの問題も、使い慣れたユーザー エクスペリエンスでパスワードを自動入力できます。
条件付き UI
パスキーとパスワード ユーザーの両方にとって効率的なユーザー エクスペリエンスを実現するために、自動入力の候補にパスキーを含めることができます。これは条件付き UI と呼ばれ、WebAuthn 標準の一部です。
ユーザーがユーザー名の入力フィールドをタップすると、自動入力候補のダイアログがポップアップ表示され、保存したパスキーとパスワード自動入力候補がハイライト表示されます。その後、ユーザーはアカウントを選択し、デバイスの画面ロックを使用してログインできます。
これにより、何も変更していないかのように既存のフォームを使用してウェブサイトにログインできます。ただし、パスキーのセキュリティ上のメリットはあります(ユーザーが持っている場合)。
仕組み
パスキーで認証するには、WebAuthn API を使用します。
パスキー認証フローの 4 つのコンポーネントは次のとおりです。
- バックエンド: 公開鍵とパスキーに関するその他のメタデータを格納するアカウント データベースを保持するバックエンド サーバー。
- フロントエンド: ブラウザと通信し、バックエンドにフェッチ リクエストを送信するフロントエンドです。
- ブラウザ: JavaScript を実行しているユーザーのブラウザ。
- 認証システム: パスキーを作成して保存するユーザーの認証システム。これは、ブラウザと同じデバイス(Windows Hello を使用している場合など)でも、スマートフォンなどの別のデバイスでもかまいません。
- ユーザーがフロントエンドにアクセスするとすぐに、パスキーで認証するためのチャレンジをバックエンドにリクエストし、
navigator.credentials.get()
を呼び出してパスキーによる認証を開始します。Promise
が返されます。 - ユーザーがログイン フィールドにカーソルを置くと、パスキーを含むパスワード自動入力ダイアログがブラウザに表示されます。ユーザーがパスキーを選択すると、認証ダイアログが表示されます。
- ユーザーがデバイスの画面ロックを使用して本人確認を行うと、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 フロントエンドに返されます。
Promise は、さまざまな理由で拒否される場合があります。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 を検索します(ユーザー アカウントを特定する必要がある場合は、userHandle
プロパティを使用します。これは、認証情報の作成時に指定した user.id
です)。保存されている公開鍵で認証情報の signature
を検証できるかどうかを確認します。そのためには、独自のコードを記述するのではなく、サーバー側のライブラリまたはソリューションを使用することをおすすめします。オープンソース ライブラリは Awesome Webauth GitHub リポジトリにあります。
一致する公開鍵で認証情報を検証したら、ユーザーをログインします。
詳しい手順については、サーバーサイド パスキー認証をご覧ください。