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

概要

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

パスキー認証フロー

  • パスキーによる認証に必要なチャレンジとその他のオプションを定義します。これらをクライアントに送信して、パスキー認証呼び出し(ウェブでは navigator.credentials.get)に渡すようにします。ユーザーがパスキー認証を確認すると、パスキー認証の呼び出しが解決され、認証情報(PublicKeyCredential)が返されます。認証情報には認証アサーションが含まれています。
で確認できます。
  • 認証アサーションを検証します。
  • 認証アサーションが有効な場合は、ユーザーを認証します。

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

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

チャレンジを作成する

実際には、チャレンジはランダムなバイトの配列であり、ArrayBuffer オブジェクトとして表されます。

// Example challenge, base64URL-encoded
weMLPOSx1VfSnMV6uPwDKbjGdKRMaUDGxeDEUTT5VN8

チャレンジの目的を達成するには、以下を行う必要があります。

  1. 同じチャレンジを複数回使用しないようにします。ログインするたびに新しい本人確認情報を生成します。成功したかどうかにかかわらず、ログインするたびに本人確認のメッセージを破棄します。一定時間経過したらチャレンジも破棄します。レスポンスで同じチャレンジを複数回受け入れないでください。
  2. チャレンジが暗号的に安全であることを確認する。推測が事実上不可能な課題でなければなりません暗号的に安全なチャレンジをサーバーサイドで作成するには、信頼できる FIDO サーバーサイド ライブラリを利用することをおすすめします。独自のチャレンジを作成する場合は、技術スタックに組み込まれている暗号機能を使用するか、暗号ユースケース向けに設計されたライブラリを探してください。たとえば、Node.js では iso-crypto、Python では secrets などです。仕様に従って、チャレンジが安全であると見なされるには、チャレンジの長さが 16 バイト以上である必要があります。

本人確認を作成したら、後で確認できるようにユーザーのセッションに保存します。

認証情報リクエストのオプションを作成する

認証情報リクエストのオプションを publicKeyCredentialRequestOptions オブジェクトとして作成します。

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

publicKeyCredentialRequestOptions には、パスキー認証に必要な情報がすべて含まれている必要があります。この情報を、publicKeyCredentialRequestOptions オブジェクトを作成する FIDO サーバーサイド ライブラリの関数に渡します。

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

  • rpId: 認証情報を関連付ける RP ID(例: example.com)。認証は、ここで指定した RP ID が認証情報に関連付けられた RP ID と一致する場合にのみ成功します。RP ID を入力するには、認証情報の登録時に publicKeyCredentialCreationOptions で設定した RP ID と同じ値を使用します。
  • challenge: 認証リクエスト時にユーザーがパスキーを保持していることを証明するために、パスキーのプロバイダが署名するデータ。チャレンジを作成するで詳細を確認します。
  • allowCredentials: この認証に使用可能な認証情報の配列。空の配列を渡して、ブラウザに表示されるリストからユーザーが利用可能なパスキーを選択できるようにします。詳細については、Fetch a challenge from the RP server(RP サーバーからチャレンジを取得する)と Discoverable credentials の詳細をご覧ください。
  • userVerification: デバイスの画面ロックを使用したユーザー確認が「必須」または「優先」かどうかを示します。または「推奨されません」。Fetch a challenge from the RP server を確認します。
  • timeout: ユーザーが認証を完了するまでにかかる時間(ミリ秒単位)。ある程度の余裕を持たせ、challenge の存続期間よりも短くする必要があります。推奨されるデフォルト値は 5 分ですが、10 分まで延長することもできます。この値は推奨範囲の範囲内です。長いタイムアウトは、ユーザーがハイブリッド ワークフローを使用する予定で、通常は少し時間がかかる場合に適しています。オペレーションがタイムアウトすると、NotAllowedError がスローされます。

publicKeyCredentialRequestOptions を作成したら、クライアントに送信します。

<ph type="x-smartling-placeholder">
</ph> サーバーによって送信された publicKeyCredentialCreationOptions
サーバーから送信されたオプション。challenge のデコードはクライアントサイドで行われます。

コードの例: create credential request options

この例では、SimpleWebAuthn ライブラリを使用しています。ここでは、認証情報リクエスト オプションの作成を generateAuthenticationOptions 関数に引き渡しています。

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

router.post('/signinRequest', csrfCheck, async (req, res) => {

  // Ensure you nest 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 {
    // Use the generateAuthenticationOptions function from SimpleWebAuthn
    const options = await generateAuthenticationOptions({
      rpID: process.env.HOSTNAME,
      allowCredentials: [],
    });
    // Save the challenge in the user session
    req.session.challenge = options.challenge;

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

ユーザーを確認してログインする

navigator.credentials.get がクライアントで正常に解決されると、PublicKeyCredential オブジェクトが返されます。

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

responseAuthenticatorAssertionResponse である。これは、RP でパスキーによる認証を試行するために必要なものを作成するというクライアントの指示に対する、パスキー プロバイダのレスポンスを表します。これには以下のものが含まれます。

  • response.authenticatorDataresponse.clientDataJSONパスキー登録のステップなど)です。
  • response.signature: これらの値に対するシグネチャが含まれます。

PublicKeyCredential オブジェクトをサーバーに送信します。

サーバーで、次の操作を行います。

<ph type="x-smartling-placeholder">
</ph> データベース スキーマ
推奨されるデータベース スキーマ。この設計について詳しくは、サーバーサイドのパスキー登録をご覧ください。
  • アサーションの検証とユーザーの認証に必要な情報を収集します。 <ph type="x-smartling-placeholder">
      </ph>
    • 認証オプションを生成したときにセッションに保存している、想定されるチャレンジを取得します。
    • 想定されるオリジンと RP ID を取得します。
    • データベースでユーザーが誰であるかを探します。検出可能な認証情報の場合、認証リクエストを行っているユーザーが誰なのかわかりません。確認するには、次の 2 つの方法があります。 <ph type="x-smartling-placeholder">
        </ph>
      • オプション 1: PublicKeyCredential オブジェクトで response.userHandle を使用する。[ユーザー] テーブルで、userHandle に一致する passkey_user_id を探します。
      • オプション 2: PublicKeyCredential オブジェクトに存在する認証情報 id を使用する。[Public key credentials] テーブルで、PublicKeyCredential オブジェクトにある認証情報 id と一致する認証情報 id を探します。次に、Users テーブルの外部キー passkey_user_id を使用して、対応するユーザーを探します。
    • 受け取った認証アサーションに一致する公開鍵認証情報をデータベースで見つけます。そのためには、[Public key credentials] テーブルで、PublicKeyCredential オブジェクト内にある認証情報 id と一致する認証情報 id を探します。
  • 認証アサーションを検証します。この確認ステップを FIDO サーバーサイド ライブラリに渡します。このライブラリは通常、この目的のためにユーティリティ関数を提供します。SimpleWebAuthn で提供されるサービス(verifyAuthenticationResponse など)その仕組みについては、付録: 認証レスポンスの検証をご覧ください。

  • 検証の成否を問わずチャレンジを削除して、リプレイ攻撃を防ぎます。

  • ユーザーのログインを行います。確認に成功したら、セッション情報を更新して、ユーザーをログイン済みとしてマークします。また、user オブジェクトをクライアントに返して、新しくログインしたユーザーに関連付けられた情報をフロントエンドで使用することもできます。

コードの例: ユーザーを確認してログインする

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

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

router.post('/signinResponse', csrfCheck, async (req, res) => {
  const response = req.body;
  const expectedChallenge = req.session.challenge;
  const expectedOrigin = getOrigin(req.get('User-Agent'));
  const expectedRPID = process.env.HOSTNAME;

  // 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 {
    // Find the credential stored to the database by the credential ID
    const cred = Credentials.findById(response.id);
    if (!cred) {
      throw new Error('Credential not found.');
    }
    // Find the user - Here alternatively we could look up the user directly
    // in the Users table via userHandle
    const user = Users.findByPasskeyUserId(cred.passkey_user_id);
    if (!user) {
      throw new Error('User not found.');
    }
    // Base64URL decode some values
    const authenticator = {
      credentialPublicKey: isoBase64URL.toBuffer(cred.publicKey),
      credentialID: isoBase64URL.toBuffer(cred.id),
      transports: cred.transports,
    };

    // Verify the credential
    const { verified, authenticationInfo } = await verifyAuthenticationResponse({
      response,
      expectedChallenge,
      expectedOrigin,
      expectedRPID,
      authenticator,
      requireUserVerification: false,
    });

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

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

    req.session.username = user.username;
    req.session['signed-in'] = 'yes';

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

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

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

認証レスポンスの検証は、次のチェックで構成されます。

  • RP ID がサイトと一致していることを確認します。
  • リクエスト元がサイトのログイン元と一致していることを確認します。Android アプリの場合は、オリジンの確認をご覧ください。
  • 入力したチャレンジをデバイスが提供できるかどうかを確認します。
  • 認証時に、RP として必須とされている要件にユーザーが準拠していることを確認します。ユーザー確認が必要な場合は、authenticatorDatauv(ユーザー確認済み)フラグが true であることを確認してください。パスキーではユーザーの存在が常に必要であるため、authenticatorDataup(ユーザー存在)フラグが true になっていることを確認します。
  • 署名を検証します。署名を検証するには、次のものが必要です。 <ph type="x-smartling-placeholder">
      </ph>
    • 署名(署名付きチャレンジ): response.signature
    • 署名の検証に使用する公開鍵。
    • 元の署名付きデータ。これが、署名の検証対象となるデータです。
    • 署名の作成に使用された暗号アルゴリズム。
で確認できます。

これらの手順について詳しくは、SimpleWebAuthn の verifyAuthenticationResponse のソースコードをご覧になるか、検証の全一覧を仕様でご確認ください。