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

概要

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

パスキー登録フロー

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

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

<ph type="x-smartling-placeholder">

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

サーバーで最初に実行する必要があるのは、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 と発行するデータを検証するより高度な手法)を使用する場合を除き、このチャレンジは関係ありません。ただし、構成証明を使用しない場合でも、チャレンジは必須項目です。その場合は、わかりやすくするために、このチャレンジを 1 つの 0 に設定できます。認証用のセキュアなチャレンジを作成する手順については、サーバーサイド パスキー認証をご覧ください。

エンコードとデコード

<ph type="x-smartling-placeholder">
</ph> サーバーによって送信された PublicKeyCredentialCreationOptions サーバーから送信される
PublicKeyCredentialCreationOptionsPublicKeyCredentialCreationOptions を HTTPS 経由で配信できるように、challengeuser.idexcludeCredentials.credentials をサーバーサイドで base64URL にエンコードする必要があります。

PublicKeyCredentialCreationOptions には ArrayBuffer のフィールドが含まれているため、JSON.stringify() ではサポートされていません。つまり現時点では、PublicKeyCredentialCreationOptions を HTTPS 経由で配信するには、一部のフィールドをサーバーで 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 = 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 });
  }
});

公開鍵を保存する

<ph type="x-smartling-placeholder">
</ph> サーバーによって送信された PublicKeyCredentialCreationOptions
navigator.credentials.createPublicKeyCredential オブジェクトを返す。

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

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

PublicKeyCredential オブジェクトをサーバーに送信します。メールが届いたら、確認します。

この確認ステップを FIDO サーバーサイド ライブラリに渡します。通常は、そのためのユーティリティ関数が用意されています。SimpleWebAuthn で提供されるサービス(verifyRegistrationResponse など)内部の処理については、付録: 登録レスポンスの検証をご覧ください。

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

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

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

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

  • Users テーブル: <ph type="x-smartling-placeholder">
      </ph>
    • user_id: プライマリ ユーザー ID。ユーザーのランダムかつ一意で永続的な ID。これを Users テーブルの主キーとして使用します。
    • username。ユーザー定義のユーザー名。編集も可能です。
    • passkey_user_id: パスキー固有の PII のないユーザー ID。登録オプションuser.id で表されます。ユーザーが後で認証を試みると、認証システムはこの passkey_user_iduserHandle の認証レスポンスで利用できるようにします。passkey_user_id を主キーとして設定しないことをおすすめします。主キーは広く使用されているため、システムでは多くの場合、事実上の PII になります。
  • 公開鍵認証情報のテーブル: <ph type="x-smartling-placeholder">
      </ph>
    • id: 認証情報 ID。これを [公開鍵の認証情報] テーブルの主キーとして使用します。
    • public_key: 認証情報の公開鍵。
    • passkey_user_id: これを外部キーとして使用して、Users テーブルとのリンクを確立します。
    • backed_up: パスキーは、パスキーのプロバイダによって同期されている場合にバックアップされます。バックアップ状態を保存すると、backed_up パスキーを保持しているユーザーのパスワードの削除を今後検討する場合に役立ちます。パスキーがバックアップされているかどうかを確認するには、authenticatorData のフラグを調べるか、この情報に簡単にアクセスできる FIDO サーバーサイドのライブラリ機能を使用します。バックアップの資格要件を保存すると、ユーザーからのお問い合わせに対応する際に便利です。
    • name: (省略可)ユーザーが認証情報にカスタム名を付けられるようにするための認証情報の表示名。
    • transports: 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 { credentialPublicKey, credentialID } = registrationInfo;

    // Existing, signed-in user
    const { user } = res.locals;
    
    // Save the credential
    await Credentials.update({
      id: base64CredentialID,
      publicKey: base64PublicKey,
      // Optional: set the platform as a default name for the credential
      // (example: "Pixel 7")
      name: req.useragent.platform, 
      transports: response.response.transports,
      passkey_user_id: user.passkey_user_id,
      backed_up: registrationInfo.credentialBackedUp
    });

    // 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 つの情報が含まれています。 <ph type="x-smartling-placeholder">
      </ph>
    • attestationStatement: 証明書を使用しない場合は関係ありません。
    • authenticatorData は、パスキー プロバイダから見たデータです。RP として authenticatorData を読み取ると、パスキー プロバイダによって参照され、create リクエスト時に返されたデータにアクセスできます。

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

  • 公開鍵認証情報自体と、その一意の認証情報 ID。
  • 認証情報に関連付けられた RP ID。
  • パスキー作成時のユーザー ステータス(ユーザーが実際に存在していたかどうか、ユーザーの確認が正常に完了したかどうか)を示すフラグ(userVerification を参照)。
  • AAGUID: パスキー プロバイダを識別します。パスキー プロバイダを表示すると、ユーザーにとって便利です。複数のパスキー プロバイダで、サービス用にパスキーを登録しているユーザーには特に有用です。

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

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

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

  • RP ID がサイトと一致していることを確認します。
  • リクエストの送信元がサイトの想定される送信元(メインサイトの URL、Android アプリ)であることを確認します。
  • ユーザー確認が必要な場合は、ユーザー確認フラグ authenticatorData.uvtrue であることを確認します。パスキーではユーザー プレゼンスが常に必要であるため、ユーザー プレゼンス フラグ authenticatorData.uptrue であることを確認します。
  • 提示した課題を、クライアントが理解できたかどうかを確認します。構成証明を使用しない場合、このチェックは重要ではありません。ただし、このチェックを実装することはベスト プラクティスです。今後構成証明を使用する場合に、コードの準備を整えることができます。
  • どのユーザーについても認証情報 ID がまだ登録されていないことを確認します。
  • パスキーのプロバイダが認証情報の作成に使用するアルゴリズムが、指定したアルゴリズム(publicKeyCredentialCreationOptions.pubKeyCredParams の各 alg フィールド内)であることを確認します。これは通常、サーバーサイド ライブラリ内で定義され、ユーザーには表示されません。これにより、ユーザーは許可することを選択したアルゴリズムにのみ登録できます。

詳しくは、SimpleWebAuthn の verifyRegistrationResponse のソースコードをご覧になるか、仕様をご確認ください。

次のステップ

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