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

概要

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

パスキー登録フロー

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

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

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

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

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

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

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

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

エンコードとデコード

サーバーから送信された PublicKeyCredentialCreationOptions
サーバーから送信された PublicKeyCredentialCreationOptions。challenge や user.id などのバイナリ フィールドは、サーバーサイドで base64URL にエンコードされるため、オプションを HTTPS 経由で JSON として配信できます。

標準の WebAuthn PublicKeyCredentialCreationOptions オブジェクトには、ArrayBuffer であるフィールドが含まれています。これらは JSON.stringify() で直接シリアル化できません。これらのオプションを HTTPS 経由で送信するには、バイナリ フィールドを base64URL 文字列としてエンコードする必要があります。

  • サーバー側: FIDO サーバーサイド ライブラリ(SimpleWebAuthn など)が、Base64URL エンコードされたフィールドを含む JSON シリアル化可能なオプション オブジェクト(PublicKeyCredentialCreationOptionsJSON など)を自動的に生成します。
  • クライアント側では、最新のブラウザは PublicKeyCredential.parseCreationOptionsFromJSON() をサポートしており、JSON オプションを PublicKeyCredentialCreationOptions に直接変換できます。@simplewebauthn/browser などのクライアントサイド ライブラリを使用する場合、startRegistration({ optionsJSON: options }) などの関数がこの変換を自動的に処理します。

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

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

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
} from '@simplewebauthn/server';
import { isoBase64URL, isoUint8Array } 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: 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: isoUint8Array.fromUTF8String(user.id),
      userName: user.username,
      userDisplayName: user.displayName || '',
      attestationType: 'none',
      excludeCredentials,
      authenticatorSelection: {
        authenticatorAttachment: 'platform',
        residentKey: 'required',
        userVerification: 'preferred',
      },
    });

    // 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.create は PublicKeyCredential オブジェクトを返します。

クライアントで 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: 個人情報を含まないパスキー固有のユーザー 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,
      credential,
      credentialBackedUp
    } = registrationInfo;

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

    const base64PublicKey = isoBase64URL.fromBuffer(credential.publicKey);

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

    // Save the credential
    await Credentials.update({
      id: credential.id,
      passkey_user_id: user.passkey_user_id,
      publicKey: base64PublicKey,
      name,
      aaguid,
      transports: credential.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 オリジン、チャレンジ、androidPackageName(クライアントが Android アプリの場合)が含まれます。RP として clientDataJSON を読み取ると、create リクエスト時にブラウザが認識した情報にアクセスできます。
  • response.attestationObject には、次の 2 つの情報が含まれています。
    • attestationStatement。これは、アテステーションを使用する場合を除き、関連しません。
    • authenticatorData は、パスキー プロバイダから見たデータです。RP として authenticatorData を読み取ると、パスキー プロバイダが参照し、create リクエスト時に返されたデータにアクセスできます。

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

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

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

付録: 登録レスポンスの検証

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

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

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

次のセクション

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