サーバーサイドのパスキー登録

概要

パスキー登録の主な手順の概要は次のとおりです。

パスキー登録フロー

  • パスキーを作成するためのオプションを定義します。クライアントに送信して、パスキー作成呼び出し(ウェブでは WebAuthn API 呼び出し navigator.credentials.create、Android では credentialManager.createCredential)に渡せるようにします。ユーザーがパスキーの作成を確認すると、パスキーの作成呼び出しが解決され、認証情報 PublicKeyCredential が返されます。
  • 認証情報を検証し、サーバーに保存します。

以降のセクションでは、各ステップの詳細について説明します。

認証情報作成オプションを作成する

サーバーで最初に行うことは、PublicKeyCredentialCreationOptions オブジェクトを作成することです。

これを行うには、FIDO サーバーサイド ライブラリを使用します。通常、これらのオプションを作成できるユーティリティ関数が提供されます。SimpleWebAuthn は、たとえば generateRegistrationOptions を提供します。

PublicKeyCredentialCreationOptions には、パスキーの作成に必要なすべての情報(ユーザーに関する情報、RP に関する情報、作成する認証情報のプロパティの設定)が含まれている必要があります。これらをすべて定義したら、PublicKeyCredentialCreationOptions オブジェクトの作成を担当する FIDO サーバーサイド ライブラリの関数に必要に応じて渡します。

PublicKeyCredentialCreationOptions のフィールドの一部は定数にできます。その他はサーバーで動的に定義する必要があります。

  • rpId: サーバーに RP ID を入力するには、example.com などのウェブ アプリケーションのホスト名を取得するサーバーサイドの関数または変数を使用します。
  • user.nameuser.displayName: これらのフィールドに入力するには、ログインしているユーザーのセッション情報(または、ユーザーが登録時にパスキーを作成している場合は新しいユーザー アカウント情報)を使用します。user.name は通常メールアドレスであり、RP ごとに一意です。user.displayName はわかりやすい名前です。すべてのプラットフォームで displayName が使用されるわけではありません。
  • user.id: アカウントの作成時に生成されるランダムで一意の文字列。編集可能なユーザー名とは異なり、永続的なものである必要があります。ユーザー ID はアカウントを識別しますが、個人を特定できる情報(PII)は含めないでください。システムにユーザー ID がすでにある可能性が高いですが、必要に応じて、パスキー専用のユーザー ID を作成して、PII が含まれないようにします。
  • excludeCredentials: パスキー プロバイダのパスキーが重複しないようにするための、既存の認証情報の ID のリスト。このフィールドに入力するには、データベースでこのユーザーの既存の認証情報を検索します。詳しくは、パスキーがすでに存在する場合に新しいパスキーが作成されないようにするをご覧ください。
  • challenge: 認証情報の登録では、パスキー プロバイダの ID とその出力データを検証するより高度な手法である構成証明を使用しない限り、チャレンジは関連しません。ただし、構成証明を使用しない場合でも、チャレンジは必須フィールドです。認証用の安全なチャレンジを作成する手順については、サーバーサイドのパスキー認証をご覧ください。

エンコードとデコード

サーバーから送信された PublicKeyCredentialCreationOptions
PublicKeyCredentialCreationOptions がサーバーから送信されます。challengeuser.idexcludeCredentials.credentials はサーバーサイドで base64URL にエンコードされ、PublicKeyCredentialCreationOptions が HTTPS で配信されるようにする必要があります。

PublicKeyCredentialCreationOptions には ArrayBuffer が含まれているため、JSON.stringify() ではサポートされていません。つまり、現時点では、HTTPS で PublicKeyCredentialCreationOptions を配信するには、サーバーで base64URL を使用して一部のフィールドを手動でエンコードし、クライアントでデコードする必要があります。

  • サーバー側では、通常、エンコードとデコードは FIDO サーバーサイド ライブラリによって処理されます。
  • クライアント側では、現時点ではエンコードとデコードを手動で行う必要があります。今後、オプションを JSON として PublicKeyCredentialCreationOptions に変換するメソッドが利用可能になるため、より簡単になります。Chrome での実装のステータスをご確認ください。

コード例: 認証情報作成オプションを作成する

この例では SimpleWebAuthn ライブラリを使用しています。ここでは、公開鍵認証情報オプションの作成を generateRegistrationOptions 関数に渡します。

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} from '@simplewebauthn/server';
import { isoBase64URL } from '@simplewebauthn/server/helpers';

router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => {
  const { user } = res.locals;
  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // `excludeCredentials` prevents users from re-registering existing
    // credentials for a given passkey provider
    const excludeCredentials = [];
    const credentials = Credentials.findByUserId(user.id);
    if (credentials.length > 0) {
      for (const cred of credentials) {
        excludeCredentials.push({
          id: isoBase64URL.toBuffer(cred.id),
          type: 'public-key',
          transports: cred.transports,
        });
      }
    }

    // Generate registration options for WebAuthn create
    const options = await generateRegistrationOptions({
      rpName: process.env.RP_NAME,
      rpID: process.env.HOSTNAME,
      userID: user.id,
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        requireResidentKey: true
      },
    });

    // Keep the challenge in the session
    req.session.challenge = options.challenge;

    return res.json(options);
  } catch (e) {
    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

公開鍵を保存する

サーバーから送信された PublicKeyCredentialCreationOptions
navigator.credentials.createPublicKeyCredential オブジェクトを返します。

クライアントで navigator.credentials.create が正常に解決された場合、パスキーが正常に作成されたことを意味します。PublicKeyCredential オブジェクトが返されます。

PublicKeyCredential オブジェクトには AuthenticatorAttestationResponse オブジェクトが含まれています。これは、パスキーを作成するというクライアントの指示に対するパスキー プロバイダのレスポンスを表します。これには、RP が後でユーザーを認証するために必要な新しい認証情報に関する情報が含まれています。AuthenticatorAttestationResponse について詳しくは、付録: AuthenticatorAttestationResponse をご覧ください。

PublicKeyCredential オブジェクトをサーバーに送信します。届きましたら、確認してください。

この検証ステップを FIDO サーバーサイド ライブラリに渡します。通常、この目的で使用できるユーティリティ関数が提供されます。たとえば、SimpleWebAuthn は verifyRegistrationResponse を提供します。バックグラウンドで何が行われているかについては、付録: 登録レスポンスの検証をご覧ください。

検証が成功したら、認証情報の情報をデータベースに保存します。これにより、ユーザーは後でその認証情報に関連付けられたパスキーで認証できるようになります。

パスキーに関連付けられた公開鍵認証情報には専用のテーブルを使用します。ユーザーが設定できるパスワードは 1 つだけですが、パスキーは複数設定できます。たとえば、Apple iCloud キーチェーン経由で同期されたパスキーと、Google パスワード マネージャー経由で同期されたパスキーなどです。

認証情報の保存に使用できるスキーマの例を次に示します。

パスキーのデータベース スキーマ

  • Users テーブル:
    • user_id: プライマリ ユーザー ID。ユーザーのランダムで一意の永続的な ID。これを Users テーブルの主キーとして使用します。
    • username。ユーザー定義のユーザー名。編集可能な場合があります。
    • passkey_user_id: パスキー固有の PII を含まないユーザー ID。登録オプションuser.id で表されます。ユーザーが後で認証を試みると、認証システムは userHandle の認証レスポンスでこの passkey_user_id を利用可能にします。passkey_user_id を主キーとして設定しないことをおすすめします。主キーは広範囲で使用されるため、システムで事実上の PII になる傾向があります。
  • 公開鍵認証情報テーブル:
    • id: 認証情報 ID。公開鍵認証情報テーブルの主キーとして使用します。
    • public_key: 認証情報の公開鍵。
    • passkey_user_id: 外部キーとして使用して、Users テーブルとのリンクを確立します。
    • backed_up: パスキー プロバイダによって同期される場合、パスキーはバックアップされます。バックアップ状態を保存しておくと、将来的に backed_up パスキーを保持しているユーザーのパスワードを削除することを検討する場合に便利です。パスキーがバックアップされているかどうかは、authenticatorData の BE フラグを調べるか、通常この情報に簡単にアクセスできる FIDO サーバーサイド ライブラリ機能を使用して確認できます。バックアップの対象となるかどうかを保存しておくと、ユーザーからの問い合わせに対応する際に役立ちます。
    • name: 省略可。ユーザーが認証情報にカスタム名を付けられるようにするための、認証情報の表示名。
    • transports: トランスポートの配列。トランスポートの保存は、認証ユーザー エクスペリエンスに役立ちます。トランスポートが利用可能な場合、ブラウザはそれに応じて動作し、パスキー プロバイダがクライアントとの通信に使用するトランスポートに一致する UI を表示できます。特に、allowCredentials が空でない再認証ユースケースの場合です。

パスキー プロバイダ、認証情報の作成時間、最終使用時間などの項目を含むその他の情報は、ユーザー エクスペリエンスの目的で保存しておくと便利です。詳しくは、パスキーのユーザー インターフェース設計をご覧ください。

コードの例: 認証情報を保存する

この例では SimpleWebAuthn ライブラリを使用しています。ここでは、登録レスポンスの検証を verifyRegistrationResponse 関数に渡します。

import { isoBase64URL } from '@simplewebauthn/server/helpers';


router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => {
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;
  const response = req.body;
  // This sample code is for registering a passkey for an existing,
  // signed-in user

  // Ensure you nest verification function calls in try/catch blocks.
  // If something fails, throw an error with a descriptive error message.
  // Return that message with an appropriate error code to the client.
  try {
    // Verify the credential
    const { verified, registrationInfo } = await verifyRegistrationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      requireUserVerification: false,
    });

    if (!verified) {
      throw new Error('Verification failed.');
    }

    const {
      aaguid,
      credentialPublicKey,
      credentialID,
      credentialBackedUp
    } = registrationInfo;

    // Name the credential based on AAGUID
    const name =
      aaguid === undefined ||
      aaguid === '000000-0000-0000-0000-00000000' ?
        req.useragent?.platform : aaguids[aaguid].name;

    const base64CredentialID = isoBase64URL.fromBuffer(credentialID);
    const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);

    // Existing, signed-in user
    const { user } = res.locals;

    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: response.response.transports,
      backed_up: credentialBackedUp,
      registered_at: new Date().getTime()
    });

    // Kill the challenge for this session
    delete req.session.challenge;

    return res.json(user);
  } catch (e) {
    delete req.session.challenge;

    console.error(e);
    return res.status(400).send({ error: e.message });
  }
});

付録: AuthenticatorAttestationResponse

AuthenticatorAttestationResponse には、次の 2 つの重要なオブジェクトが含まれています。

  • response.clientDataJSONクライアント データの JSON バージョンです。ウェブでは、ブラウザに表示されるデータです。これには、RP オリジン、チャレンジ、クライアントが Android アプリの場合は androidPackageName が含まれます。RP として clientDataJSON を読み取ると、create リクエスト時にブラウザが確認した情報にアクセスできます。
  • response.attestationObject には次の 2 つの情報が含まれています。
    • attestationStatement。アテステーションを使用しない限り、関連性はありません。
    • authenticatorData は、パスキー プロバイダから見たデータです。RP として authenticatorData を読み取ると、パスキー プロバイダが認識し、create リクエスト時に返されるデータにアクセスできます。

authenticatorData には、新しく作成されたパスキーに関連付けられた公開鍵認証情報に関する重要な情報が含まれています。

  • 公開鍵認証情報自体と、その一意の認証情報 ID。
  • 認証情報に関連付けられた RP ID。
  • パスキーが作成されたときのユーザー ステータスを記述するフラグ。ユーザーが実際に存在したかどうか、ユーザーが正常に確認されたかどうか(userVerification の詳細を参照)。
  • AAGUID は、Google パスワード マネージャーなどのパスキー プロバイダの識別子です。AAGUID に基づいて、パスキー プロバイダを特定し、パスキー管理ページに名前を表示できます。(AAGUID でパスキー プロバイダを特定するを参照)

authenticatorDataattestationObject 内にネストされていますが、構成されている情報は、構成証明を使用するかどうかにかかわらず、パスキーの実装に必要です。authenticatorData はエンコードされ、バイナリ形式でエンコードされたフィールドを含んでいます。通常、サーバーサイド ライブラリが解析とデコードを処理します。サーバーサイド ライブラリを使用していない場合は、getAuthenticatorData() クライアントサイドを活用して、サーバーサイドでの解析とデコードの作業を軽減することを検討してください。

付録: 登録応答の検証

登録レスポンスの検証は、内部的には次のチェックで構成されています。

  • RP ID がサイトと一致していることを確認します。
  • リクエストのオリジンがサイトの想定されるオリジン(メインサイトの URL、Android アプリ)であることを確認します。
  • ユーザー確認が必要な場合は、ユーザー確認フラグ authenticatorData.uvtrue であることを確認します。
  • 通常、ユーザー プレゼンス フラグ authenticatorData.uptrue であることが想定されますが、認証情報が条件付きで作成された場合は false であることが想定されます。
  • クライアントが、指定したチャレンジを提供できたことを確認します。構成証明を使用しない場合、このチェックは重要ではありません。ただし、このチェックを実装することはベスト プラクティスです。これにより、将来的に証明書を使用することになった場合に、コードをすぐに使用できるようになります。
  • 認証情報 ID がどのユーザーにも登録されていないことを確認します。
  • パスキー プロバイダが認証情報の作成に使用したアルゴリズムが、リストに記載されているアルゴリズム(publicKeyCredentialCreationOptions.pubKeyCredParams の各 alg フィールド。通常はサーバーサイド ライブラリ内で定義され、ユーザーには表示されません)であることを確認します。これにより、ユーザーは管理者が許可したアルゴリズムでのみ登録できるようになります。

詳しくは、SimpleWebAuthn の verifyRegistrationResponse のソースコードをご覧になるか、仕様で検証の完全なリストをご覧ください。

次のステップ

サーバーサイドのパスキー認証