ウェブアプリでフォームの自動入力を使用してパスキーを実装する

1. 始める前に

パスワードの代わりにパスキーを使用すると、ウェブサイトでのユーザー アカウントの安全性、シンプルさ、使いやすさを向上させることができます。パスキーを導入すると、ユーザーは、指紋、顔認証、デバイスの PIN など、デバイスの画面ロック機能を使用して、ウェブサイトやアプリにログインできるようになります。パスキーはユーザー アカウントに関連付けて作成する必要があります。また、その公開鍵は、ユーザーがパスキーを使用してログインする前にサーバーに保存しておく必要があります。

この Codelab では、基本的なフォームベースのユーザー名とパスワードのログインを変更して、パスキーをサポートするログインを実装します。このログインには次の要素が含まれます。

  • ユーザーがログインした後にパスキーを作成するボタン。
  • 登録済みのパスキーのリストを表示する UI。
  • 既存のログイン フォームで、フォームの自動入力により登録済みのパスキーを使用してユーザーがログインできるようにする機能。

前提条件

学習内容

  • パスキーを作成する方法。
  • パスキーを使用してユーザーを認証する方法。
  • フォームでログイン オプションとしてパスキーを提示する方法。

必要なもの

次のいずれかの組み合わせが必要です。

  • Android 9 以降を搭載している Android デバイス(生体認証センサー付きが望ましい)と Google Chrome。
  • Windows 10 以降を搭載している Windows デバイスと Chrome。
  • iOS 16 以降を搭載している iPhone または iPadOS 16 以降を搭載している iPad と Safari 16 以降。
  • macOS Ventura 以降を搭載している Apple デスクトップ デバイスと Safari 16 以降または Chrome。

2. セットアップする

この Codelab では Glitch というサービスを使用します。ブラウザのみを使用して、クライアント サイドとサーバーサイドの JavaScript コードを編集し、デプロイできます。

プロジェクトを開く

  1. Glitch でプロジェクトを開きます。
  2. [Remix] をクリックして Glitch プロジェクトをフォークします。
  3. Glitch の下部にあるナビゲーション メニューで、[Preview] > [Preview in a new window] をクリックします。ブラウザで別のタブが開きます。

Glitch の下部にあるナビゲーション メニューの [Preview in a new window] ボタン

ウェブサイトの初期状態を確認する

  1. プレビュータブでランダムなユーザー名を入力し、[Next] をクリックします。
  2. ランダムなパスワードを入力して [Sign-in] をクリックします。パスワードは無視され、認証されてホームページが表示されます。
  3. 表示名を変更したい場合は、その操作を行います。初期状態でできることは以上です。
  4. [Sign out] をクリックします。

初期状態では、ユーザーはログインするたびにパスワードを入力する必要があります。ユーザーがデバイスの画面ロック機能を使用してログインできるように、このフォームにパスキーのサポートを追加します。https://passkeys-codelab.glitch.me/ で最終状態を試すことができます。

パスキーの仕組みの詳細については、パスキーの仕組みをご覧ください。

3. パスキーを作成する機能を追加する

ユーザーがパスキーを使用して認証できるようにするには、パスキーを作成、登録してその公開鍵をサーバーに保存する機能をユーザーに提供する必要があります。

パスキーの作成時には、パスキーのユーザー確認ダイアログが表示されます。

ユーザーがパスワードでログインした後にパスキーを作成できるようにするのがいいでしょう。また、ユーザーがパスキーを作成するための UI と、/home ページで登録済みのすべてのパスキーのリストを表示する UI もあったほうがいいでしょう。次のセクションでは、パスキーの作成と登録を行う関数を作成します。

registerCredential() 関数を作成する

  1. Glitch で public/client.js ファイルを開き、一番下までスクロールします。
  2. 対応するコメントの後に、次の registerCredential() 関数を追加します。

public/client. js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to create a passkey: Create a credential.

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.

};

この関数はサーバー側でパスキーの作成と登録を行います。

サーバー エンドポイントからチャレンジとその他のオプションを取得する

パスキーを作成する前に、WebAuthn に渡すパラメータ(チャレンジを含む)をサーバーにリクエストする必要があります。WebAuthn は、ユーザーがパスキーを作成し、そのパスキーで認証できるようにするためのブラウザ API です。うれしいことに、この Codelab には、これらのパラメータを返すサーバー エンドポイントがすでに用意されています。

  • サーバー エンドポイントからチャレンジとその他のオプションを取得するには、registerCredential() 関数本体の中の対応するコメントの後に、次のコードを追加します。

public/client.js

// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/registerRequest');

次のコード スニペットには、サーバーから受け取るサンプルのオプションが含まれています。

{
  challenge: *****,
  rp: {
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal', 'hybrid'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

WebAuthn の仕様では、サーバーとクライアントの間のプロトコルは規定されていません。ただし、この Codelab のサーバーは、WebAuthn の navigator.credentials.create() API に渡される PublicKeyCredentialCreationOptions ディクショナリにできるだけ近い JSON を返すように設計されています。

次の表に、PublicKeyCredentialCreationOptions ディクショナリの重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

challenge

この登録を行うためにサーバーで生成されたチャレンジであり、形式は ArrayBuffer オブジェクトです。これは必須ですが、アテステーションを行う場合を除き、登録時には使用されません。アテステーションは高度なトピックであり、この Codelab では扱いません。

user.id

ユーザーの一意の ID。この値は、メールアドレスやユーザー名などの個人を特定する情報を含まない ArrayBuffer オブジェクトである必要があります。アカウントごとに生成するランダムな 16 バイトの値でも十分に機能します。

user.name

このフィールドには、メールアドレスやユーザー名など、ユーザーが認識できるアカウントの一意の識別子を格納します。この識別子はアカウント選択画面に表示されます(ユーザー名を使用する場合はパスワード認証の値と同じ値を使用します)。

user.displayName

これはオプションのフィールドであり、アカウントのわかりやすい名前を表します。一意である必要はなく、ユーザーが選択した名前でも構いません。このフィールドに含めるのに適した値がウェブサイトにない場合は、空の文字列を渡してください。ブラウザによっては、アカウント選択画面にこの値が表示されることがあります。

rp.id

リライング パーティ(RP)ID はドメインです。ウェブサイトは、この値として自身のドメイン、または登録可能なサフィックスを指定できます。たとえば、RP のオリジンが https://login.example.com:1337 の場合は、RP ID として login.example.comexample.com のいずれかを指定できます。RP ID が example.com として指定されている場合、ユーザーは login.example.com で認証を行うことができます。また、example.com の他の任意のサブドメインでも認証を行うことができます。

pubKeyCredParams

このフィールドには、RP でサポートされる公開鍵アルゴリズムを指定します。[{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}] を設定することをおすすめします。この設定は、P-256 の ECDSA と RSA PKCS#1 がサポートされていることを意味します。これらをサポートすることで、全範囲をカバーできます。

excludeCredentials

すでに登録されている認証情報 ID のリストを指定します。これにより、同じデバイスが 2 回登録されないようにします。このフィールドを指定する場合、transports メンバーには、各認証情報の登録時に getTransports() 関数を呼び出した結果が含まれている必要があります。

authenticatorSelection.authenticatorAttachment

"platform" という値を設定します。これは、プラットフォーム デバイスに内蔵されている認証システムが必要であることを示します。これを指定することで、USB セキュリティ キーなどを挿入するよう求めるメッセージがユーザーに表示されないようにします。

authenticatorSelection.requireResidentKey

ブール値 true を設定します。検出可能な認証情報(レジデントキー)は、サーバーが認証情報の ID を提示しなくても使用できるため、自動入力に対応しています。

authenticatorSelection.userVerification

"preferred" という値を設定します。この値はデフォルト値であるため、このパラメータを省略してもかまいません。デバイスの画面ロックを使用するユーザー確認が、"required""preferred""discouraged" のいずれであるかを示します。"preferred" の値を設定すると、デバイスが対応できる場合はユーザー確認がリクエストされます。

認証情報を作成する

  1. registerCredential() 関数本体の中の対応するコメントの後で、Base64URL でエンコードされた一部のパラメータをバイナリに戻します。具体的には、user.idchallenge の文字列、ならびに excludeCredentials 配列に含まれる id 文字列のインスタンスをバイナリに戻します。

public/client.js

// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);

if (options.excludeCredentials) {
  for (let cred of options.excludeCredentials) {
    cred.id = base64url.decode(cred.id);
  }
}
  1. 次の行で、authenticatorSelection.authenticatorAttachment"platform" に、authenticatorSelection.requireResidentKeytrue に設定します。これにより、検出可能な認証情報の機能を持つプラットフォーム認証システム(そのデバイス自体)のみが許容されるようになります。

public/client.js

// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
  authenticatorAttachment: 'platform',
  requireResidentKey: true
}
  1. 次の行で、navigator.credentials.create() メソッドを呼び出して認証情報を作成します。

public/client.js

// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
  publicKey: options,
});

この呼び出しを行うと、ブラウザはデバイスの画面ロックを使用してユーザーの本人確認を試みます。

認証情報をサーバー エンドポイントに登録する

ユーザーが本人確認を行うと、パスキーが作成され、保存されます。ウェブサイトは、公開鍵が含まれた認証情報オブジェクトを受け取ります。この公開鍵をサーバーに送信してパスキーを登録できます。

次のコード スニペットには、認証情報オブジェクトの例が含まれています。

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "attestationObject": *****,
    "transports": ["internal", "hybrid"]
  },
  "authenticatorAttachment": "platform"
}

次の表に、PublicKeyCredential オブジェクトの重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

id

作成されたパスキーの、Base64URL でエンコードされた ID。この ID によって、ブラウザは認証時に一致するパスキーがデバイス内にあるかどうかを判別できます。この値は、バックエンドのデータベースに保存する必要があります。

rawId

認証情報 ID の ArrayBuffer オブジェクト バージョン。

response.clientDataJSON

エンコードされたクライアント データの ArrayBuffer オブジェクト。

response.attestationObject

エンコードされたアテステーション オブジェクトの ArrayBuffer。これには、RP ID、フラグ、公開鍵などの重要な情報が含まれます。

response.transports

デバイスがサポートしているトランスポートのリスト: "internal" は、デバイスがパスキーをサポートしていることを意味します。"hybrid" は、別のデバイスでの認証もサポートされていることを意味します。

authenticatorAttachment

パスキー対応デバイスでこの認証情報が作成されると、"platform" が返されます。

認証情報オブジェクトをサーバーに送信する手順は次のとおりです。

  1. 認証情報を文字列としてサーバーに送信するため、認証情報のバイナリ パラメータを Base64URL としてエンコードします。

public/client.js

// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
  credential.authenticatorAttachment = cred.authenticatorAttachment;
}

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);

// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];

credential.response = {
  clientDataJSON,
  attestationObject,
  transports
};
  1. 次の行で、オブジェクトをサーバーに送信します。

public/client.js

return await _fetch('/auth/registerResponse', credential);

プログラムを実行すると、サーバーは HTTP code 200 を返します。これは、認証情報が登録されたことを意味します。

これで registerCredential() 関数が完成しました。

このセクションの解答コードを確認する

public/client.js

// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {

  // TODO: Add an ability to create a passkey: Obtain the challenge and other options from server endpoint.
  const options = await _fetch('/auth/registerRequest');

  // TODO: Add an ability to create a passkey: Create a credential.
  // Base64URL decode some values.

  options.user.id = base64url.decode(options.user.id);
  options.challenge = base64url.decode(options.challenge);

  if (options.excludeCredentials) {
    for (let cred of options.excludeCredentials) {
      cred.id = base64url.decode(cred.id);
    }
  }

  // Use platform authenticator and discoverable credential.
  options.authenticatorSelection = {
    authenticatorAttachment: 'platform',
    requireResidentKey: true
  }

  // Invoke the WebAuthn create() method.
  const cred = await navigator.credentials.create({
    publicKey: options,
  });

  // TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
  if (cred.authenticatorAttachment) {
    credential.authenticatorAttachment = cred.authenticatorAttachment;
  }

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const attestationObject =
  base64url.encode(cred.response.attestationObject);

  // Obtain transports.
  const transports = cred.response.getTransports ?
  cred.response.getTransports() : [];

  credential.response = {
    clientDataJSON,
    attestationObject,
    transports
  };

  return await _fetch('/auth/registerResponse', credential);
};

4. パスキーの認証情報の登録と管理を行うための UI を作成する

registerCredential() 関数を作成しましたが、これを呼び出すボタンが必要です。また、登録済みのパスキーのリストを表示する必要もあります。

/home ページに表示される登録済みのパスキー

プレースホルダの HTML を追加する

  1. Glitch で views/home.html ファイルを開きます。
  2. 対応するコメントの後に、パスキー登録のボタンとパスキーのリストを表示する UI プレースホルダを追加します。

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

div#list 要素はリストのプレースホルダです。

パスキーのサポートを確認する

パスキーをサポートするデバイスを使用するユーザーにのみパスキーを作成するオプションを表示するには、最初に WebAuthn が利用可能かどうかを確認する必要があります。利用可能である場合、[Create a passkey] ボタンを表示するには、hidden クラスを削除する必要があります。

環境がパスキーをサポートしているかどうかを確認するには、次の手順を行います。

  1. views/home.html ファイルの対応するコメントの後に、window.PublicKeyCredentialPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailablePublicKeyCredential.isConditionalMediationAvailabletrue であるかどうかを判断する条件文を記述します。

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  1. 条件の内部で、デバイスがパスキーを作成できるかどうかを確認し、次にフォームの自動入力でパスキーを提示できるかどうかを確認します。

views/home.html

try {
  const results = await Promise.all([

    // Is platform authenticator available in this browser?
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

    // Is conditional UI available in this browser?
    PublicKeyCredential.isConditionalMediationAvailable()
  ]);
  1. すべての条件が満たされている場合は、パスキーを作成するボタンを表示します。それ以外の場合は、警告メッセージを表示します。

views/home.html

    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

登録済みのパスキーのリストを表示する

  1. 登録済みのパスキーをサーバーから取得してリストにして表示する renderCredentials() 関数を定義します。うれしいことに、ログインしているユーザーの登録済みのパスキーを取得する /auth/getKeys サーバー エンドポイントがすでに用意されています。

views/home.html

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
    <mwc-list>
      ${res.map(cred => html`
        <mwc-list-item>
          <div class="list-item">
            <div class="entity-name">
              <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}"
            data-name="${cred.name || 'Unnamed' }" @click="${rename}"
            icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}"
            icon="delete"></mwc-icon-button>
          </div>
         </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};
  1. 次の行で、ユーザーが /home ページにアクセスしたら、初期化として renderCredentials() 関数を呼び出して、登録済みのパスキーを表示します。

views/home.html

renderCredentials();

パスキーを作成して登録する

パスキーを作成して登録するには、前に実装した registerCredential() 関数を呼び出す必要があります。

[Create a passkey] ボタンをクリックしたときに registerCredential() 関数をトリガーする手順は次のとおりです。

  1. ファイル内のプレースホルダの HTML の後にある次の import ステートメントを見つけます。

views/home.html

import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
} from '/client.js';
  1. import ステートメントの本文の最後に registerCredential() 関数を追加します。

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';
  1. ファイル内の対応するコメントの後で register() 関数を定義します。この関数は、registerCredential() 関数を呼び出し、ローディング UI を表示し、登録後に renderCredentials() を呼び出します。これにより、ブラウザでパスキーが作成されていることが明確に示されます。また、問題発生時にはエラー メッセージが表示されます。

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  1. register() 関数の内部で例外をキャッチします。デバイスにパスキーがすでに存在する場合、navigator.credentials.create() メソッドは InvalidStateError エラーをスローします。これを調べるには excludeCredentials 配列を使用します。この場合は、適切なメッセージをユーザーに表示します。また、ユーザーが認証ダイアログをキャンセルした場合は NotAllowedError エラーがスローされます。この場合は、何も表示せずにエラーを無視します。

views/home.html

  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};
  1. register() 関数の後にある行で、[Create a passkey] ボタンの click イベントに register() 関数をアタッチします。

views/home.html

createPasskey.addEventListener('click', register);

このセクションの解答コードを確認する

views/home.html

​​<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
  <h3 class="mdc-typography mdc-typography--headline6"> Your registered
  passkeys:</h3>
  <div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>

views/home.html

// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
  $,
  _fetch,
  loading,
  updateCredential,
  unregisterCredential,
  registerCredential
} from '/client.js';

views/home.html

// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');

// Feature detections
if (window.PublicKeyCredential &&
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {
    const results = await Promise.all([

      // Is platform authenticator available in this browser?
      PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),

      // Is conditional UI available in this browser?
      PublicKeyCredential.isConditionalMediationAvailable()
    ]);
    if (results.every(r => r === true)) {

      // If conditional UI is available, reveal the Create a passkey button.
      createPasskey.classList.remove('hidden');
    } else {

      // If conditional UI isn't available, show a message.
      $('#message').innerText = 'This device does not support passkeys.';
    }
  } catch (e) {
    console.error(e);
  }
} else {

  // If WebAuthn isn't available, show a message.
  $('#message').innerText = 'This device does not support passkeys.';
}

// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
  const res = await _fetch('/auth/getKeys');
  const list = $('#list');
  const creds = html`${res.length > 0 ? html`
  <mwc-list>
    ${res.map(cred => html`
      <mwc-list-item>
        <div class="list-item">
          <div class="entity-name">
            <span>${cred.name || 'Unnamed' }</span>
          </div>
          <div class="buttons">
            <mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
            <mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
          </div>
        </div>
      </mwc-list-item>`)}
  </mwc-list>` : html`
  <mwc-list>
    <mwc-list-item>No credentials found.</mwc-list-item>
  </mwc-list>`}`;
  render(creds, list);
};

renderCredentials();

// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
  try {

    // Start the loading UI.
    loading.start();

    // Start creating a passkey.
    await registerCredential();

    // Stop the loading UI.
    loading.stop();

    // Render the updated passkey list.
    renderCredentials();
  } catch (e) {

    // Stop the loading UI.
    loading.stop();

    // An InvalidStateError indicates that a passkey already exists on the device.
    if (e.name === 'InvalidStateError') {
      alert('A passkey already exists for this device.');

    // A NotAllowedError indicates that the user canceled the operation.
    } else if (e.name === 'NotAllowedError') {
      Return;

    // Show other errors in an alert.
    } else {
      alert(e.message);
      console.error(e);
    }
  }
};

createPasskey.addEventListener('click', register);

試してみる

ここまでの手順をすべて完了していれば、ウェブサイトでパスキーを作成、登録、表示する機能が実装されています。

これらの機能を試す手順は次のとおりです。

  1. プレビュータブで、ランダムなユーザー名とパスワードでログインします。
  2. [Create a passkey] をクリックします。
  3. デバイスの画面ロックを使用して本人確認を行います。
  4. パスキーが登録済みであり、ウェブページの [Your registered passkeys] セクションに表示されていることを確認します。

/home ページに表示される登録済みのパスキー。

登録済みのパスキーの名前変更と削除

リストに表示されている登録済みのパスキーの名前変更と削除を行うことができます。この Codelab ではそれらのコードが実装されているため、動作はコードで確認できます。

パソコンで Chrome を使用している場合は、chrome://settings/passkeys で登録済みのパスキーを削除できます。Android の場合は、設定のパスワード マネージャーで登録済みのパスキーを削除できます。

他のプラットフォームで登録済みのパスキーの名前変更と削除を行う方法については、それぞれのプラットフォームのサポートページをご覧ください。

5. パスキーによる認証機能を追加する

ユーザーはパスキーを作成して登録できるようになりました。ウェブサイトでの安全な認証方法としてパスキーを使用する準備が整いました。あとは、パスキーによる認証機能をウェブサイトに追加する必要があります。

authenticate() 関数を作成する

  • public/client.js ファイルの対応するコメントの後に、authenticate() という関数を作成します。この関数は、ユーザーをローカルで確認してから、サーバーとの間で確認します。

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.

  // TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.

};

サーバー エンドポイントからチャレンジとその他のオプションを取得する

ユーザーに認証を求める前に、WebAuthn で渡すパラメータ(チャレンジを含む)をサーバーにリクエストする必要があります。

  • authenticate() 関数の内部の対応するコメントの後で _fetch() 関数を呼び出して、サーバーに POST リクエストを送信します。

public/client.js

// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');

この Codelab のサーバーは、WebAuthn の navigator.credentials.get() API に渡される PublicKeyCredentialRequestOptions ディクショナリにできるだけ近い JSON を返すように設計されています。次のコード スニペットには、受け取るオプションのサンプルが含まれています。

{
  "challenge": *****,
  "rpId": "passkeys-codelab.glitch.me",
  "allowCredentials": []
}

次の表に、PublicKeyCredentialRequestOptions ディクショナリの重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

challenge

サーバーで生成されたチャレンジで、形式は ArrayBuffer オブジェクトです。これはリプレイ攻撃の防止に必要です。レスポンスで同じチャレンジを 2 回受け取ることはありません。これは CSRF トークンとみなすことができます。

rpId

RP ID はドメインです。ウェブサイトは、この値として自身のドメイン、または登録可能なサフィックスを指定できます。この値は、パスキーの作成時に使用された rp.id パラメータと一致する必要があります。

allowCredentials

このプロパティは、この認証の対象となる認証システムの検出に使用されます。ブラウザでアカウント選択画面を表示できるようにするには、空の配列を渡すか、未指定にします。

userVerification

"preferred" という値を設定します。この値はデフォルト値であるため、このパラメータを省略してもかまいません。デバイスの画面ロックを使用するユーザー確認が、"required""preferred""discouraged" のいずれであるかを示します。"preferred" の値を設定すると、デバイスが対応できる場合はユーザー確認がリクエストされます。

ユーザーをローカルで確認し、認証情報を取得する

  1. authenticate() 関数内部の対応するコメントの後で、challenge パラメータをバイナリに戻します。

public/client.js

// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
  1. allowCredentials パラメータに空の配列を渡して、ユーザーが認証を行う際にアカウント選択画面が表示されるようにします。

public/client.js

// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];

アカウント選択画面では、パスキーとともに保存されているユーザーの情報を使用します。

  1. mediation: 'conditional' オプションを指定して navigator.credentials.get() メソッドを呼び出します。

public/client.js

// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
  publicKey: options,

  // Request a conditional UI.
  mediation: 'conditional'
});

このオプションは、フォーム自動入力の一環として条件付きでパスキーを提示するようブラウザに指示します。

認証情報を検証する

ユーザーがローカルで本人確認を行ったら、サーバーで検証可能なシグネチャが含まれる認証情報オブジェクトを受け取ります。

次のコード スニペットには、サンプルの PublicKeyCredential オブジェクトが含まれています。

{
  "id": *****,
  "rawId": *****,
  "type": "public-key",
  "response": {
    "clientDataJSON": *****,
    "authenticatorData": *****,
    "signature": *****,
    "userHandle": *****
  },
  authenticatorAttachment: "platform"
}

次の表に、PublicKeyCredential オブジェクトの重要なパラメータを示します(すべてを網羅しているわけではありません)。

パラメータ

説明

id

認証済みパスキー認証情報の、Base64URL でエンコードされた ID。

rawId

認証情報 ID の ArrayBuffer オブジェクト バージョン。

response.clientDataJSON

クライアント データの ArrayBuffer オブジェクト。このフィールドには、RP サーバーで検証する必要があるチャレンジやオリジンなどの情報が含まれます。

response.authenticatorData

認証システムデータの ArrayBuffer オブジェクト。このフィールドには RP ID などの情報が含まれます。

response.signature

シグネチャの ArrayBuffer オブジェクト。この値は、認証情報の核となる情報であり、サーバーで検証する必要があります。

response.userHandle

作成時に設定されたユーザー ID を含む ArrayBuffer オブジェクト。サーバーが使用する ID 値をサーバー側で選択する必要がある場合、またはバックエンドで認証情報 ID のインデックス作成が行われないようにしたい場合は、認証情報 ID の代わりにこの値を使用できます。

authenticatorAttachment

この認証情報がローカル デバイスから取得された場合は、"platform" という文字列を返します。それ以外の場合(特にユーザーがスマートフォンを使用してログインする場合)は、"cross-platform" という文字列を返します。ユーザーがスマートフォンを使用してログインする必要がある場合は、ローカル デバイスでパスキーを作成するようユーザーにプロンプトを表示します。

認証情報オブジェクトをサーバーに送信する手順は次のとおりです。

  1. authenticate() 関数内部の対応するコメントの後で、認証情報のバイナリ パラメータをエンコードして、文字列としてサーバーに送信できるようにします。

public/client.js

// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;

// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData = base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);

credential.response = {
  clientDataJSON,
  authenticatorData,
  signature,
  userHandle,
};
  1. オブジェクトをサーバーに送信します。

public/client.js

return await _fetch(`/auth/signinResponse`, credential);

プログラムを実行すると、サーバーは HTTP code 200 を返します。これは、認証情報が検証されたことを意味します。

これで authentication() 関数が完成しました。

このセクションの解答コードを確認する

public/client.js

// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {

  // TODO: Add an ability to authenticate with a passkey: Obtain the
  challenge and other options from the server endpoint.
  const options = await _fetch('/auth/signinRequest');

  // TODO: Add an ability to authenticate with a passkey: Locally verify
  the user and get a credential.
  // Base64URL decode the challenge.
  options.challenge = base64url.decode(options.challenge);

  // The empty allowCredentials array invokes an account selector
  by discoverable credentials.
  options.allowCredentials = [];

  // Invoke the WebAuthn get() function.
  const cred = await navigator.credentials.get({
    publicKey: options,

    // Request a conditional UI.
    mediation: 'conditional'
  });

  // TODO: Add an ability to authenticate with a passkey: Verify the credential.
  const credential = {};
  credential.id = cred.id;
  credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
  credential.type = cred.type;

  // Base64URL encode some values.
  const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
  const authenticatorData =
  base64url.encode(cred.response.authenticatorData);
  const signature = base64url.encode(cred.response.signature);
  const userHandle = base64url.encode(cred.response.userHandle);

  credential.response = {
    clientDataJSON,
    authenticatorData,
    signature,
    userHandle,
  };

  return await _fetch(`/auth/signinResponse`, credential);
};

6. ブラウザの自動入力にパスキーを追加する

ユーザーが再度アクセスした場合は、できる限り簡単かつ安全にログインできるようにする必要があります。ログインページに [Sign in with a passkey] ボタンを追加すると、ユーザーはこのボタンを押し、ブラウザのアカウント選択画面でパスキーを選択し、画面ロックを使用して本人確認を行うことができます。

ただし、パスワードからパスキーへの移行は、すべてのユーザーに対して一挙に行われるわけではありません。すべてのユーザーがパスキーに移行するまでパスワードをなくすことはできないので、それまではパスワード ベースのログイン フォームを残す必要があります。しかし、パスワード フォームとパスキーボタンを両方表示すると、ユーザーはログインでいずれを使用するかを選択しなければなりません。単純でわかりやすいログイン プロセスが理想的です。

このような場合に「条件付き UI」が役に立ちます。条件付き UI は WebAuthn の機能です。パスワードの他に、自動入力項目としてパスキーを提示するフォーム入力フィールドを作成できます。ユーザーは、自動入力の候補でパスキーをタップすると、デバイスの画面ロックを使用してローカルで本人確認を行うよう求められます。ユーザーの操作はパスワード ベースのログインの操作とほぼ同じであるため、シームレスなユーザー エクスペリエンスになります。

フォームの自動入力の一環として提示されるパスキー。

条件付き UI を有効にする

条件付き UI を有効にするには、入力フィールドの autocomplete 属性に webauthn トークンを追加するだけです。このトークンを設定し、mediation: 'conditional' 文字列を指定して navigator.credentials.get() メソッドを呼び出すと、条件付きで画面ロック UI をトリガーできます。

  • 条件付き UI を有効にするには、view/index.html ファイルの対応するコメントの後で、既存のユーザー名入力フィールドを次の HTML に置き換えます。

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus />

機能の検出、WebAuthn の呼び出し、条件付き UI の有効化

  1. view/index.html ファイルの対応するコメントの後で、既存の import ステートメントを次のコードに置き換えます。

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate
} from "/client.js";

このコードは、前に実装した authenticate() 関数をインポートします。

  1. window.PulicKeyCredential オブジェクトが使用可能であることと、PublicKeyCredential.isConditionalMediationAvailable() メソッドが true 値を返すことを確認したら、authenticate() 関数を呼び出します。

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (
  window.PublicKeyCredential &&
  PublicKeyCredential.isConditionalMediationAvailable
) {
  try {

    // Is conditional UI available in this browser?
    const cma =
      await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $("#username").value = user.username;
        loading.start();
        location.href = "/home";
      } else {
        throw new Error("User not found.");
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== "NotAllowedError") {
      console.error(e);
      alert(e.message);
    }
  }
}

このセクションの解答コードを確認する

view/index.html

<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
  type="text"
  id="username"
  class="mdc-text-field__input"
  aria-labelledby="username-label"
  name="username"
  autocomplete="username webauthn"
  autofocus
/>

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
  $,
  _fetch,
  loading,
  authenticate
} from '/client.js';

view/index.html

// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
// Is WebAuthn avaiable in this browser?
if (window.PublicKeyCredential &&
    PublicKeyCredential.isConditionalMediationAvailable) {
  try {

    // Is a conditional UI available in this browser?
    const cma= await PublicKeyCredential.isConditionalMediationAvailable();
    if (cma) {

      // If a conditional UI is available, invoke the authenticate() function.
      const user = await authenticate();
      if (user) {

        // Proceed only when authentication succeeds.
        $('#username').value = user.username;
        loading.start();
        location.href = '/home';
      } else {
        throw new Error('User not found.');
      }
    }
  } catch (e) {
    loading.stop();

    // A NotAllowedError indicates that the user canceled the operation.
    if (e.name !== 'NotAllowedError') {
      console.error(e);
      alert(e.message);
    }
  }
}

試してみる

パスキーの作成、登録、表示、認証をウェブサイトで実装しました。

これらの機能を試す手順は次のとおりです。

  1. プレビュータブを開きます。
  2. 必要に応じてログアウトします。
  3. ユーザー名のテキスト ボックスをクリックします。ダイアログが表示されます。
  4. ログインに使用するアカウントを選択します。
  5. デバイスの画面ロックを使用して本人確認を行います。/home ページにリダイレクトされ、ログインされます。

保存したパスワードまたはパスキーによる本人確認を求めるダイアログ。

7. 完了

これでこの Codelab は終了です。ご不明な点がございましたら、FIDO-DEV のメーリング リストまたは StackOverflow(passkey タグを使用)でお問い合わせください。

その他の情報