あなたのアプリケーションが受け付けるパスワードは、推測され、別のサイトで使い回され、偽のログインページに入力され、あるいはデータベースのバックアップから流出する可能性があります。Passkey はこの 4 つの問題を一度に取り除きます。ユーザーはすでにスマートフォンやノートパソコンのロックを解除している指紋、顔、PIN でサインインし、あなたのサーバーには盗む価値のある秘密がもう存在しません。
新しいログインコンポーネントの概要では Passkey は 5 つの箇条書きしか占めませんでしたが、SAML の記事では自前の identity provider を運用する企業向けのサインインを扱いました。この記事はパスワードレス側の話です。TsgcWSAPIServer_WebAuthn がどのように Passkey を登録し、ユーザー名なしでサインインさせ、ブラウザの自動入力リストに Passkey を表示し、各 credential を自前のデータベースに保存するかを説明します。
Passkey とは何か
Passkey はユーザーの authenticator が作成する鍵ペアです。Windows Hello、iCloud キーチェーン、Google パスワードマネージャー、パスワードマネージャー、あるいは FIDO2 セキュリティキーなどが該当します。秘密鍵は authenticator から外に出ることは決してありません。サーバー側は公開鍵だけを保存するため、Passkey のテーブルが流出しても誰もサインインできません。すべての署名はサイトのドメインに紐づいているため、よく似たフィッシングドメインは利用できるものを何も得られません。ここでいうフィッシング耐性とはこのことです。
WebAuthn の用語では、Passkey は検出可能な credentialであり、resident key とも呼ばれます。authenticator は userHandle を秘密鍵の隣に保持しており、そのためユーザーはユーザー名を入力せずにサインインでき、ブラウザは自力であなたのサイトの Passkey を一覧表示できます。
検出可能な credential を要求する
WebAuthn にはセキュアコンテキストが必要なので、ページを https で配信するか、開発中は localhost から配信し、WebAuthnOptions.RelyingParty にブラウザが表示するホスト名を設定してください。登録オプションが求める residentKey は WebAuthnOptions.DefaultOptions.Registration.DiscoverableCredential から決まります。
waundcPreferred、デフォルト値です。authenticator は可能なときに検出可能な credential を作成します。waundcRequired。検出可能な credential だけが受け入れられます。Passkey とユーザー名なしサインインに使ってください。waundcDiscouraged。authenticator はサーバー側の credential を作成すべきで、サインインにはユーザー名が必要になります。
単一の登録リクエストは、discoverable_credential フィールドに required、preferred、discouraged のいずれかを設定することで、それでもデフォルト値を上書きできます。
ユーザー名なしでサインインする
ユーザー名を空にして認証オプションを要求してください。この場合、オプションには allowCredentials リストが含まれず、ブラウザはあなたの relying party 向けに保持している Passkey を表示し、ユーザーはその中から一つを選びます。入力するものが何もないので、入力ミスも起こりません。
今回はサーバー側が credential を選んでいないため、より多くのことを検証します。レスポンスには userHandle が含まれていなければならず、userHandle は署名した credential に属していなければならず、アプリケーションが知らない credential は拒否されます。
自動入力リストの中の Passkey
自動入力、つまり conditional mediation は、保存済みのパスワードの隣にあるユーザー名フィールドの候補リストに、あなたのサイトの Passkey を並べます。これはユーザーを穏やかに移行させる方法です。パスワードを使う人にはログインページがこれまでどおり機能し、Passkey を持つ人はリストからそれを選びます。
- ユーザー名の入力欄に
autocomplete="username webauthn"を追加してください。 - ページに
/sgcWebAuthn.jsを読み込んでください。このコンポーネントは、EndpointsOptions.WebAuthnに設定されたエンドポイントから自身でこのファイルを配信します。 - ページが読み込まれたら、ユーザー名なしのオプションをリクエストし、
startAuthentication(options, true)を呼び出してください。ユーザーがリストから Passkey を選ぶと promise が解決します。
自動入力には conditional mediation に対応したブラウザが必要で、現在では Chrome、Edge、Safari の最新版がこれに当たります。それ以外のブラウザでは browserSupportsWebAuthnAutofill() が False を返すため、ページには“Passkey でサインイン”ボタンも用意しておいてください。
複数の Passkey、一つの userHandle
実際のユーザーは複数の Passkey を持っています。ノートパソコンに一つ、スマートフォンに一つ、両方を失くしてしまう日のためのセキュリティキーもあるかもしれません。それぞれを同じユーザー名で登録してください。そのユーザー名がすでにサーバーの知っている credential を持っている場合、新しい登録はその userHandle(user.id)を再利用するため、そのアカウントのすべての Passkey が一つの userHandle を共有し、すべて同じユーザーにつながります。
サーバーは稼働中に登録された credential と、AddCredential で追加した credential を把握しています。Passkey がデータベースに存在する場合は、各アカウントが一つの userHandle を保つように起動時に追加してください。ユーザーが実際にユーザー名を入力した場合、認証オプションはそのユーザーのすべての Passkey を allowCredentials に列挙し、authenticator は自分が持っているものを使います。
Passkey を自前のデータベースに保存する
このコンポーネントは credential を永続化しません。あなたの Passkey はユーザーテーブルの隣に置かれるべきもので、4 つのイベントが両者をつなぎます。
OnWebAuthnRegistrationSuccessful。新しい credential レコード、たとえばaCredentialRecord.AsJSONを、そのCredentialIdとUsernameとともに保存してください。OnWebAuthnAuthenticationOptionsRequest。ユーザー名がある場合は、そのユーザーの Passkey をCredentialRecordsに追加してください。ユーザー名がない場合は何も追加しないでください。OnWebAuthnAuthenticationGetCredential。ブラウザが選んだ credential が今回の ceremony のリストにないときに発生し、これはユーザー名なしのサインインと自動入力のサインインすべてに当てはまります。credential を検索し、aCredentialRecordを埋め、Foundを設定してください。OnWebAuthnAuthenticationSuccessful。aAuthentication.Credential.CredentialRecordの新しいSignCountとBackupStateを保存してから、セッションを作成してください。
Passkey の保存と読み込みは、それぞれ短いハンドラー 2 つで済みます。
uses
sgcWebAuthn_Classes;
// registration: store the whole record, keyed by its credential id
procedure TForm1.WebAuthnWebAuthnRegistrationSuccessful(Sender: TObject;
const aRegistration: TsgcWebAuthn_Registration;
const aCredentialRecord: TsgcWebAuthn_CredentialRecord; var Accept: Boolean);
begin
DBInsertPasskey(aCredentialRecord.CredentialId, aCredentialRecord.Username,
aCredentialRecord.AsJSON);
end;
// usernameless and autofill sign-in: the browser chose the passkey,
// find it by its credential id and hand it back to the server
procedure TForm1.WebAuthnWebAuthnAuthenticationGetCredential(Sender: TObject;
const aCredentialId: string;
const aCredentialRecord: TsgcWebAuthn_CredentialRecord; var Found: Boolean);
var
vJSON: string;
begin
Found := DBFindPasskey(aCredentialId, vJSON);
if Found then
aCredentialRecord.ReadJSON(vJSON);
end;
DBInsertPasskey と DBFindPasskey は、あなた自身のデータアクセスコードを表しています。返すレコードには要求された CredentialId が含まれていなければならず、ceremony がユーザー名から始まった場合は同じ Username も含まれていなければなりません。そうでなければサインインは失敗します。サーバーは authenticator が送る userHandle と比較するため、保存するレコードには UserId も保持してください。イベントはサーバーの接続スレッド内で実行されるため、サーバーの他の場所と同じ方法で共有リソースを保護してください。
同期型かデバイス固定型か
authenticator データ内のフラグは、受け取った Passkey の種類を教えてくれます。credential レコードはこれを BackupEligible(BE フラグ)と BackupState(BS フラグ)として保持します。
- BackupEligible が True、BackupState が True。プラットフォームまたはパスワードマネージャーによってバックアップされ、ユーザーの他のデバイスでも利用できる同期型の Passkey です。
- BackupEligible が True、BackupState が False。まだバックアップされていない複数デバイス対応の Passkey です。
- BackupEligible が False、BackupState が False。セキュリティキーや、デバイスから決して出ない TPM キーのような、デバイス固定型の Passkey です。そのデバイスを失うことは credential を失うことを意味するため、ユーザーに 2 つ目の Passkey の登録を提案してください。
ポリシーによってこれらを異なる扱いにすることもできます。たとえば管理者アカウントではデバイス固定型のセキュリティキーだけを受け入れる、といった具合です。サインインのたびに、サーバーは BS フラグが立っていて BE フラグが立っていないレスポンスを拒否し、BE フラグが保存済みの BackupEligible と異なるレスポンスも拒否します。credential の eligibility は決して変わらないからです。新しい BS フラグは BackupState にコピーされ、それを保存する場所が OnWebAuthnAuthenticationSuccessful です。
署名カウンターと複製された authenticator
一部の authenticator はサインインのたびに署名カウンターを増やします。レスポンス内のカウンター、または保存済みの SignCount がゼロでない場合、レスポンス内のカウンターは保存済みの値より大きくなければなりません。そうでなければサインインは拒否されます。同じ鍵で応答する 2 つの authenticator というのは、まさに複製された authenticator の姿だからです。
この検証は、サインインのたびにカウンターを保存し、保存済みの値が常に前にしか進まない場合にのみ機能します。同期型の Passkey は通常毎回 0 を報告するため、その Passkey についてはこの検証がオフになります。カウンターを確実に増やさない authenticator がある場合は、それらを受け入れるために WebAuthnOptions.AllowSignCountLessOrEqualStoredValue を True に設定してください。それでも保存済みの値が下がることは決してありません。
デモを試す
デモ Demos\26.Authentication\01.Passkeys は完全な relying party で、TsgcWSAPIServer_WebAuthn を取り付けた TsgcWebSocketHTTPServer が https://localhost:5443 で小さなログインページを配信します。上記の 4 つのイベントを通じて、各 Passkey を独自の passkeys.json ファイルに保存します。
- デモをビルドし、libcrypto-3.dll と libssl-3.dll を実行ファイルの隣に置いてください。デモフォルダーの中にあります。
- host は 127.0.0.1、port は 5443、relying party は localhost のままにして、Start をクリックしてください。
- Open Browser をクリックし、自己署名のテスト証明書を承認してください。
- ユーザー名を入力して Register passkey をクリックしてください。同じユーザー名で 2 つ目の Passkey を登録してください。フォームには各 Passkey の種類(同期型かデバイス固定型か)と署名カウンターが一覧表示されます。
- Sign in without user name をクリックして Passkey を選んでください。サーバーは
OnWebAuthnAuthenticationGetCredentialを通じてそれを見つけ、ログにユーザーが表示されます。 - ページを再読み込みし、ユーザー名の入力欄をクリックしてください。Passkey が自動入力リストに表示されます。一つ選んでサインインしてください。
- アプリケーションを再起動してください。Passkey は passkeys.json から再び読み込まれ、これはストレージがコンポーネントではなくあなたのアプリケーションに属していることを示しています。
ドキュメント
入手方法
TsgcWSAPIServer_WebAuthn は、Delphi および C++ Builder 向け sgcWebSockets の Enterprise エディションと All-Access エディションに含まれており、同じコンポーネントは sgcWebSockets .NET の一部でもあります。認証機能だけが必要な場合は、sgcAuth パックに他のログインコンポーネントと一緒に含まれています。SGC Auth パレットにあり、フォームにドロップするまで既存のアプリケーションでは何も変わりません。
次に読む
- Passkey・SAML SSO・LDAP・TOTP 2FA で実現する Delphi ログイン
- Entra ID・Okta・AD FS で実現する Delphi の SAML シングルサインオン
- WebAuthn、パスキー、そしてパスワードの終焉
動画で見る
eSeGeCe チャンネルに“Passkeys in Delphi: passwordless login with WebAuthn”という短い動画があります。IDE 内のコード、バックアップフラグと署名カウンターが何を教えてくれるか、そして https://localhost で動くデモを紹介しています。ユーザーが 2 つの Passkey を登録し、その後ユーザー名を入力せずにサインインする様子が収められています。
ご質問やフィードバック、ログインページに Passkey を追加する手助けが必要ですか?お問い合わせください。コードを書いた本人から返信が届きます。
