---
url: 'https://www.corbado.com/ja/blog/passkey-tutorial-passkey-jissou-houhou'
title: 'パスキーチュートリアル：Webアプリにパスキーを実装する方法'
description: 'このチュートリアルでは、Webアプリにパスキーを実装する方法を解説します。Node.js (TypeScript)、SimpleWebAuthn、Vanilla HTML / JavaScript、MySQLを使用します。'
lang: 'ja'
author: 'Vincent Delitz'
date: '2025-06-17T16:16:36.586Z'
lastModified: '2026-03-27T07:03:27.597Z'
keywords: 'パスキー チュートリアル, パスキー 実装'
category: 'Passkeys Implementation'
---

# パスキーチュートリアル：Webアプリにパスキーを実装する方法

## 1. はじめに：パスキーを実装する方法

このチュートリアルでは、パスキーの実装に取り組む皆様をサポートし、ウェブサイトにパスキーを追加するためのステップバイステップガイドを提供します。

優れたウェブサイトやアプリを構築する上で、モダンで堅牢、かつユーザーフレンドリーな認証は不可欠です。パスキーは、この課題に対する答えとして登場しました。ログインの新しい標準として、従来のパスワードの欠点がない未来を約束し、真にパスワードレスなログイン体験（安全なだけでなく、非常に便利）を提供します。

パスキーの可能性を真に示しているのは、パスキーが得てきた支持です。[Chrome](https://www.corbado.com/ja/blog/digital-credentials-api)、[Firefox](https://www.corbado.com/ja/blog/digital-credentials-api)、[Safari](https://www.corbado.com/ja/blog/digital-credentials-api)、Edgeといったすべての主要なブラウザ、そしてApple、Microsoft、Googleといったすべての主要なデバイスメーカーがサポートを組み込んでいます。この満場一致の支持は、パスキーがログインの新しい標準であることを示しています。

はい、Webアプリケーションにパスキーを統合するためのチュートリアルはすでに存在します。[React](https://www.corbado.com/blog/react-passkeys)、[Vue.js](https://www.corbado.com/blog/vuejs-passkeys)、[Next.js](https://www.corbado.com/blog/nextjs-passkeys)のようなフロントエンドフレームワーク向けであれ、課題を軽減し、パスキー実装を迅速化するために設計されたガイドが豊富にあります。しかし、**ミニマリスティックでベアメタル**な**エンドツーエンドのチュートリアル**は不足しています。多くの開発者から、**Webアプリ向けのパスキー実装**を明確にするチュートリアルを求める声が寄せられました。

まさにこの理由から、私たちはこのガイドを作成しました。私たちの目的は、**フロントエンド、バックエンド、そしてデータベース層**（後者は深刻な問題を引き起こす可能性があるにもかかわらず、しばしば見過ごされがちです）を網羅する、パスキーの最小限の実行可能なセットアップを作成することです。

このチュートリアルの終わりには、以下のことができる最小限の実行可能なWebアプリケーションを構築しているでしょう：

- パスキーを作成する
- パスキーを使用してログインする

お急ぎの方やリファレンスが必要な方のために、コードベース全体は[GitHub](https://github.com/corbado/passkey-tutorial)で公開しています。

最終的な成果物がどのように見えるか気になりますか？こちらが最終プロジェクトのプレビューです（見た目は非常にベーシックですが、興味深い部分は水面下にあります）：

![Passkey Tutorial Login Screen](https://www.corbado.com/website-assets/6572cd2ed5d547903c8ad74d_passkey_tutorial_register_login_screen_f55a0f5ae9.png)

コードやプロジェクトの一部は、もっと違った方法や洗練された方法で実現できることを十分に承知していますが、私たちは本質的な部分に焦点を当てたいと考えました。そのため、意図的にシンプルでパスキー中心の内容にしています。

**本番のウェブサイトにパスキーを追加するには？**

これはパスキー認証の非常に最小限の例です。以下の項目は、このチュートリアルでは考慮/実装されていないか、非常に基本的なもののみです：

- 条件付きUI / 条件付きメディエーション / パスキー自動入力
- デバイス管理
- セッション管理
- アカウントへの複数デバイスの安全な追加
- 下位互換性
- 適切なクロスプラットフォームおよびクロスデバイスサポート
- フォールバック認証
- 適切なエラー処理
- パスキー管理ページ

これらの機能すべてを完全にサポートするには、膨大な開発工数が必要です。興味のある方には、パスキー開発者の誤解に関するこの記事をご覧になることをお勧めします。

## 2. パスキーを統合するための前提条件

パスキーの実装に深く入る前に、必要なスキルとツールを見てみましょう。始めるために必要なものは以下の通りです：

### 2.1 フロントエンド：Vanilla HTML & JavaScript

ウェブの構成要素であるHTML、CSS、JavaScriptのしっかりとした理解が不可欠です。私たちは意図的に物事を単純に保ち、最新のJavaScriptフレームワークを避け、Vanilla
JavaScript /
HTMLに依存しました。私たちが使用する唯一のより高度なものは、WebAuthnラッパーライブラリの[@simplewebauthn/browser](https://simplewebauthn.dev/docs/packages/browser/)です。

### 2.2 バックエンド：TypeScriptによるNode.js (Express) + SimpleWebAuthn

バックエンドには、TypeScriptで書かれた[Node.js](https://www.corbado.com/blog/nodejs-passkeys)
(Express)サーバーを使用します。また、[SimpleWebAuthnのWebAuthnサーバー実装](https://simplewebauthn.dev/docs/packages/server)（`@simplewebauthn/server`と`@simplewebauthn/typescript-types`）を使用することにしました。利用可能なWebAuthnサーバー実装は数多くあるため、もちろんこれらのいずれかを使用することもできます。私たちはTypeScriptのWebAuthnサーバーを選択したため、基本的な[Node.js](https://www.corbado.com/blog/nodejs-passkeys)とnpmの知識が必要です。

### 2.3 データベース：MySQL

すべてのユーザーデータとパスキーの公開鍵はデータベースに保存されます。データベース技術として[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)を選択しました。[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)とリレーショナルデータベースの基礎的な理解があると役立ちますが、個々のステップをガイドします。

以下では、WebAuthnとパスキーという用語を、公式には同じ意味ではないかもしれませんが、しばしば同じ意味で使います。特にコード部分での理解を深めるため、この前提で進めます。

これらの前提条件が整えば、パスキーの世界に飛び込む準備は万端です。

## 3. アーキテクチャ概要：パスキー実装例

コードと設定に入る前に、構築しようとしているシステムのアーキテクチャを見てみましょう。以下に、私たちが設定するアーキテクチャの内訳を示します：

- **フロントエンド：**
  ユーザー登録（パスキーの作成）用と認証（パスキーを使用したログイン）用の2つのボタンで構成されます。
- **デバイスとブラウザ：**
  フロントエンドでアクションがトリガーされると、デバイスとブラウザが関与します。これらはパスキーの作成と検証を容易にし、ユーザーとバックエンドの間の仲介役として機能します。
- **バックエンド：**
  バックエンドは、私たちのアプリケーションで真の魔法が繰り広げられる場所です。フロントエンドから開始されたすべてのリクエストを処理します。このプロセスには、パスキーの作成と検証が含まれます。バックエンド操作の中核にあるのはWebAuthnサーバーです。名前が示すものとは異なり、これはスタンドアロンのサーバーではありません。代わりに、WebAuthn標準を実装するライブラリまたはパッケージです。2つの主要な機能は次のとおりです：**登録（サインアップ）：新規ユーザーがパスキーを作成する場所、および認証（ログイン）：既存のユーザーがパスキーを使用してログインする場所。**
  最も単純な形式では、WebAuthnサーバーは4つの公開APIエンドポイントを提供し、これらは登録用の2つと認証用の2つの2つのカテゴリに分かれています。これらは特定の形式でデータを受信するように設計されており、そのデータはWebAuthnサーバーによって処理されます。WebAuthnサーバーは、必要なすべての暗号操作を担当します。注意すべき重要な点は、これらのAPIエンドポイントはHTTPS経由で提供されなければならないということです。
- **MySQLデータベース：**
  ストレージのバックボーンとして機能する[MySQL](https://www.corbado.com/blog/passkey-webauthn-database-guide)データベースは、ユーザーデータとそれに対応するクレデンシャルを保持する責任があります。

このアーキテクチャの概要により、アプリケーションのコンポーネントがどのように構成されているかの概念的なマップが得られるはずです。先に進むにつれて、これらの各コンポーネントについてさらに深く掘り下げ、そのセットアップ、設定、および相互作用について詳しく説明します。

以下の図は、登録（サインアップ）時のプロセスフローを示しています：

![Passkey Sign-Up Process Chart](https://www.corbado.com/website-assets/6572cd4d3243003bb3589e88_passkey_sign_up_process_chart_b73d643b4c.png)

以下の図は、認証（ログイン）時のプロセスフローを示しています：

![Passkey Login Process Chart](https://www.corbado.com/website-assets/6572cd5f04fd73a7ff5d501b_passkey_login_process_chart_a44262b767.png)

さらに、プロジェクトの構造は以下の通りです（最も重要なファイルのみ）：

```
passkeys-tutorial
├── src                         # すべてのバックエンドTypeScriptソースコードを格納
│   ├── controllers             # 特定のタイプのリクエストを処理するためのビジネスロジック
│   │   ├── authentication.ts   # パスキー認証ロジック
│   │   └── registration.ts     # パスキー登録ロジック
│   ├── middleware
│   │   ├── customError.ts      # 標準化された方法でカスタムエラーメッセージを追加
│   │   └── errorHandler.ts     # 一般的なエラーハンドラ
│   ├── public
│   │   ├── index.html          # フロントエンドのメインHTMLファイル
│   │   ├── css
│   │   │   └── style.css       # 基本的なスタイリング
│   │   └── js
│   │       └── script.js       # JavaScriptロジック（WebAuthn APIを含む）
│   ├── routes                  # APIルートとそのハンドラの定義
│   │   └── routes.ts           # 特定のパスキールート
│   ├── services
│   │   ├── credentialService.ts# credentialテーブルとのやり取り
│   │   └── userService.ts      # userテーブルとのやり取り
│   ├── utils                   # ヘルパー関数とユーティリティ
│   |   ├── constants.ts        # いくつかの定数（例：rpID）
│   |   └── utils.ts            # ヘルパー関数
│   ├── database.ts             # Node.jsからMySQLデータベースへの接続を作成
│   ├── index.ts                # Node.jsサーバーのエントリポイント
│   └── server.ts               # すべてのサーバー設定を管理
├── config.json                 # Node.jsプロジェクトのいくつかの設定
├── docker-compose.yml          # Dockerコンテナのサービス、ネットワーク、ボリュームを定義
├── Dockerfile                  # プロジェクトのDockerイメージを作成
├── init-db.sql                 # MySQLデータベーススキーマを定義
├── package.json                # Node.jsプロジェクトの依存関係とスクリプトを管理
└── tsconfig.json               # TypeScriptがコードをコンパイルする方法を設定
```

## 4. MySQLデータベースのセットアップ

パスキーを実装する際、データベースのセットアップは重要な要素です。私たちのアプローチでは、MySQLを実行するDockerコンテナを使用し、信頼性の高いテストとデプロイに不可欠な、単純で隔離された環境を提供します。

私たちのデータベーススキーマは意図的に最小限に抑えられており、2つのテーブルしかありません。このシンプルさが、より明確な理解と容易なメンテナンスを助けます。

**詳細なテーブル構造**

**1. Credentialsテーブル：**
パスキー認証の中心となるこのテーブルは、パスキーのクレデンシャルを保存します。重要な列：

- **credential_id：**
  各クレデンシャルの一意の識別子。このフィールドに正しいデータ型を選択することは、フォーマットエラーを避けるために不可欠です。
- **public_key：**
  各クレデンシャルの公開鍵を保存します。`credential_id`と同様に、適切なデータ型とフォーマットが重要です。

**2. Usersテーブル：** ユーザーアカウントを対応するクレデンシャルにリンクします。

最初のテーブルを `credentials`
と名付けたことに注意してください。これは私たちの経験上、また他のライブラリが推奨するより適切な名前だからです（SimpleWebAuthnが
`authenticator` や `authenticator_device` と名付けることを提案しているのとは対照的です）。

`credential_id` と `public_key`
のデータ型は非常に重要です。誤ったデータ型、エンコーディング、またはフォーマット（特にBase64とBase64URLの違いは一般的なエラーの原因です）からエラーが発生することが多く、これが登録（サインアップ）または認証（ログイン）プロセス全体を中断させる可能性があります。

これらのテーブルを設定するために必要なすべてのSQLコマンドは、`init-db.sql`ファイルに含まれています。このスクリプトにより、迅速でエラーのないデータベースの初期化が保証されます。

より高度なケースでは、`credential_device_type` や `credential_backed_up`
を追加して、クレデンシャルに関する詳細情報を保存し、ユーザーエクスペリエンスを向上させることができます。ただし、このチュートリアルではそれを行いません。

```sql filename="init-db.sql"
CREATE TABLE users
(
    id       VARCHAR(255) PRIMARY KEY,
    username VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE credentials
(
    id            INT AUTO_INCREMENT PRIMARY KEY,
    user_id       VARCHAR(255) NOT NULL,
    credential_id VARCHAR(255) NOT NULL,
    public_key    TEXT         NOT NULL,
    counter       INT          NOT NULL,
    transports    VARCHAR(255),
    FOREIGN KEY (user_id) REFERENCES users (id)
);
```

このファイルを作成した後、プロジェクトのルートレベルに新しい `docker-compose.yml`
ファイルを作成します：

```yaml filename="docker-compose.yml"
version: "3.1"

services:
    db:
        image: mysql
        command: --default-authentication-plugin=mysql_native_password
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: my-secret-pw
            MYSQL_DATABASE: webauthn_db
        ports:
            - "3306:3306"
        volumes:
            - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
```

このファイルは、ポート3306でMySQLデータベースを起動し、定義されたデータベース構造を作成します。ここで使用されているデータベースの名前とパスワードは、デモンストレーション目的でシンプルに保たれていることに注意してください。本番環境では、セキュリティを強化するためにより複雑なクレデンシャルを使用する必要があります。

次に、Dockerコンテナの実行に移ります。この時点では、`docker-compose.yml`ファイルにはこの単一のコンテナしか含まれていませんが、後でさらにコンポーネントを追加します。コンテナを起動するには、次のコマンドを使用します：

```bash
docker compose up -d
```

コンテナが起動したら、データベースが期待どおりに機能しているかを確認する必要があります。ターミナルを開き、次のコマンドを実行してMySQLデータベースと対話します：

```bash
docker exec -it <container ID> mysql -uroot -p
```

ルートパスワードの入力を求められます。この例では `my-secret-pw`
です。ログイン後、`webauthn_db`
データベースを選択し、次のコマンドを使用してテーブルを表示します：

```sql
use
webauthn_db;
show
tables;
```

この段階で、スクリプトで定義した2つのテーブルが表示されるはずです。最初はこれらのテーブルは空であり、データベースのセットアップが完了し、パスキー実装の次のステップに進む準備ができたことを示しています。

## 5. パスキーの実装：バックエンド統合手順

バックエンドは、あらゆるパスキーアプリケーションの中核であり、フロントエンドからのユーザー認証リクエストを処理する中央ハブとして機能します。登録（サインアップ）および認証（ログイン）リクエストを処理するためにWebAuthnサーバーライブラリと通信し、ユーザーのクレデンシャルを保存および取得するためにMySQLデータベースと対話します。以下では、すべてのリクエストを処理する公開APIを公開するTypeScriptを使用した[Node.js](https://www.corbado.com/blog/nodejs-passkeys)
(Express)でバックエンドをセットアップする方法をガイドします。

### 5.1 Node.js (Express) サーバーの初期化

まず、プロジェクト用の新しいディレクトリを作成し、ターミナルまたはコマンドプロンプトを使用してそのディレクトリに移動します。

コマンドを実行します

```bash
npx create-express-typescript-application passkeys-tutorial
```

これにより、TypeScriptで書かれたNode.js
(Express)アプリの基本的なコードスケルトンが作成され、これをさらなる適応に使用できます。

プロジェクトには、追加でインストールする必要があるいくつかの主要なパッケージが必要です：

- **@simplewebauthn/server：**
  ユーザー登録（サインアップ）や認証（ログイン）などのWebAuthn操作を容易にするサーバーサイドライブラリ。
- **express-session：**
  [Express.js](https://www.corbado.com/blog/nodejs-passkeys)のミドルウェアで、セッションを管理し、サーバーサイドのセッションデータを保存し、Cookieを処理します。
- **uuid：**
  アプリケーションで一意のキーや識別子を作成するためによく使用される、汎用一意識別子（UUID）を生成するユーティリティ。
- **mysql2：**
  MySQL用のNode.jsクライアントで、MySQLデータベースへの接続とクエリ実行機能を提供します。

新しいディレクトリに切り替えて、次のコマンドでそれらをインストールします（必要なTypeScriptの型もインストールします）：

```bash
cd passkeys-tutorial
npm install @simplewebauthn/server mysql2 uuid express-session
@types/express-session @types/uuid
```

すべてが正しくインストールされたことを確認するには、次を実行します

```bash
npm run dev:nodemon
```

これにより、Node.jsサーバーが開発モードでNodemonと共に起動し、ファイルが変更されるたびにサーバーが自動的に再起動します。

![Passkey Tutorial Start App](https://www.corbado.com/website-assets/6572cd86e1fa2982352c7ca1_passkey_tutorial_start_app_b442392b4d.png)

**トラブルシューティングのヒント：**
エラーが発生した場合は、`package.json`ファイルで`ts-node`をバージョン10.8.1に更新し、`npm i`を実行して更新をインストールしてみてください。

`server.ts`ファイルには、Expressアプリケーションの基本的なセットアップとミドルウェアが含まれています。パスキー機能を統合するには、以下を追加する必要があります：

- **ルート：**
  パスキーの登録（サインアップ）と認証（ログイン）のための新しいルートを定義します。
- **コントローラー：** これらのルートのロジックを処理するコントローラーを作成します。
- **ミドルウェア：** リクエストとエラー処理のためのミドルウェアを統合します。
- **サービス：** データベースでデータを取得および保存するためのサービスを構築します。
- **ユーティリティ関数：** 効率的なコード操作のためのユーティリティ関数を含めます。

これらの機能強化は、アプリケーションのバックエンドでパスキー認証を有効にするための鍵となります。これらは後で設定します。

### 5.2 MySQLデータベース接続

[セクション4](#4-setup-of-the-mysql-database)でデータベースを作成して起動した後、バックエンドがMySQLデータベースに接続できることを確認する必要があります。そのため、`/src`フォルダに新しい`database.ts`ファイルを作成し、次の内容を追加します：

```ts filename="database.ts"
import mysql from "mysql2";

// Create a MySQL pool
const pool = mysql.createPool({
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    waitForConnections: true,
    connectionLimit: 10,
    queueLimit: 0,
});

// Promisify for Node.js async/await.
export const promisePool = pool.promise();
```

このファイルは、後でサーバーがデータベースにアクセスするために使用されます。

### 5.3 アプリサーバーの設定

`config.json`を簡単に見てみましょう。そこには、アプリケーションを実行するポートと環境の2つの変数がすでに定義されています：

```json filename="config.json"
{
    "PORT": 8080,
    "NODE_ENV": "development"
}
```

`package.json`はそのままにしておき、次のようになっているはずです：

```json filename="package.json"
{
    "name": "passkeys-tutorial",
    "version": "0.0.1",
    "description": "passkeys-tutorial initialised with create-express-typescript-application.",
    "main": "src/index.ts",
    "scripts": {
        "build": "tsc",
        "start": "node ./build/src/index.js",
        "dev": "ts-node ./src/index.ts",
        "dev:nodemon": "nodemon -w src -e ts,json -x ts-node ./src/index.ts",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": ["express", "typescript"],
    "devDependencies": {
        "@types/cors": "^2.8.17",
        "@types/express": "^4.17.21",
        "@types/morgan": "^1.9.9",
        "@types/node": "^14.18.63",
        "@typescript-eslint/eslint-plugin": "^4.33.0",
        "@typescript-eslint/parser": "^4.33.0",
        "eslint": "^7.32.0",
        "nodemon": "^2.0.22",
        "ts-node": "^10.8.1",
        "typescript": "^4.9.5"
    },
    "dependencies": {
        "@simplewebauthn/server": "^8.3.5",
        "@types/express-session": "^1.17.10",
        "@types/uuid": "^9.0.7",
        "cors": "^2.8.5",
        "env-cmd": "^10.1.0",
        "express": "^4.18.2",
        "express-session": "^1.17.3",
        "fs": "^0.0.1-security",
        "helmet": "^4.6.0",
        "morgan": "^1.10.0",
        "mysql2": "^3.6.5",
        "uuid": "^9.0.1"
    }
}
```

`index.ts`は次のようになります：

```ts filename="index.ts"
import app from "./server";
import config from "../config.json";

// Start the application by listening to specific port
const port = Number(process.env.PORT || config.PORT || 8080);
app.listen(port, () => {
    console.info("Express application started on port: " + port);
});
```

`server.ts`では、さらにいくつかのことを適応させる必要があります。さらに、ユーザーが認証するための一次的なチャレンジを保存するために、何らかの一時的なキャッシュ（例：redis、memcache、またはexpress-session）が必要です。私たちは`express-session`を使用することに決め、`express-session`モジュールを先頭で宣言して、`express-session`が機能するようにします。さらに、ルーティングを合理化し、エラー処理を一旦削除します（これは後でミドルウェアに追加されます）：

```ts filename="server.ts"
import express, { Express } from "express";
import morgan from "morgan";
import helmet from "helmet";
import cors from "cors";
import config from "../config.json";
import { router as passkeyRoutes } from "./routes/routes";
import session from "express-session";

const app: Express = express();

declare module "express-session" {
    interface SessionData {
        currentChallenge?: string;
        loggedInUserId?: string;
    }
}

/************************************************************************************
 *                              Basic Express Middlewares
 ***********************************************************************************/
app.set("json spaces", 4);
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(
    session({
        // @ts-ignore
        secret: process.env.SESSION_SECRET,
        saveUninitialized: true,
        resave: false,
        cookie: {
            maxAge: 86400000,
            httpOnly: true, // Ensure to not expose session cookies to clientside scripts
        },
    }),
);

// Handle logs in console during development
if (process.env.NODE_ENV === "development" || config.NODE_ENV === "development") {
    app.use(morgan("dev"));
    app.use(cors());
}

// Handle security and origin in production
if (process.env.NODE_ENV === "production" || config.NODE_ENV === "production") {
    app.use(helmet());
}

/************************************************************************************
 *                               Register all routes
 ***********************************************************************************/
app.use("/api/passkey", passkeyRoutes);

app.use(express.static("src/public"));

export default app;
```

### 5.4 クレデンシャルサービスとユーザーサービス

作成した2つのテーブルのデータを効果的に管理するために、新しい`src/services`ディレクトリに2つの異なるサービス、`authenticatorService.ts`と`userService.ts`を開発します。

各サービスは[CRUD](https://www.corbado.com/ja/blog/react-nodejs-express-mysql-crud-app)（作成、読み取り、更新、削除）メソッドをカプセル化し、モジュール化された整理された方法でデータベースと対話できるようにします。これらのサービスは、オーセンティケータテーブルとユーザーテーブルのデータの保存、取得、更新を容易にします。これらの必要なファイルの構造は次のようになります：

`userService.ts`は次のようになります：

```ts filename="userService.ts"
import { promisePool } from "../database"; // Adjust the import path as necessary
import { v4 as uuidv4 } from "uuid";

export const userService = {
    async getUserById(userId: string) {
        const [rows] = await promisePool.query("SELECT * FROM users WHERE id = ?", [
            userId,
        ]);
        // @ts-ignore
        return rows[0];
    },

    async getUserByUsername(username: string) {
        try {
            const [rows] = await promisePool.query(
                "SELECT * FROM users WHERE username = ?",
                [username],
            );
            // @ts-ignore
            return rows[0];
        } catch (error) {
            return null;
        }
    },

    async createUser(username: string) {
        const id = uuidv4();
        await promisePool.query("INSERT INTO users (id, username) VALUES (?, ?)", [
            id,
            username,
        ]);
        return { id, username };
    },
};
```

`credentialService.ts`は次のようになります：

```ts filename="credentialService.ts"
import { promisePool } from "../database";
import type { AuthenticatorDevice } from "@simplewebauthn/typescript-types";

export const credentialService = {
    async saveNewCredential(
        userId: string,
        credentialId: string,
        publicKey: string,
        counter: number,
        transports: string,
    ) {
        try {
            await promisePool.query(
                "INSERT INTO credentials (user_id, credential_id, public_key, counter, transports) VALUES (?, ?, ?, ?, ?)",
                [userId, credentialId, publicKey, counter, transports],
            );
        } catch (error) {
            console.error("Error saving new credential:", error);
            throw error;
        }
    },

    async getCredentialByCredentialId(
        credentialId: string,
    ): Promise<AuthenticatorDevice | null> {
        try {
            const [rows] = await promisePool.query(
                "SELECT * FROM credentials WHERE credential_id = ? LIMIT 1",
                [credentialId],
            );
            // @ts-ignore
            if (rows.length === 0) return null;
            // @ts-ignore
            const row = rows[0];
            return {
                userID: row.user_id,
                credentialID: row.credential_id,
                credentialPublicKey: row.public_key,
                counter: row.counter,
                transports: row.transports ? row.transports.split(",") : [],
            } as AuthenticatorDevice;
        } catch (error) {
            console.error("Error retrieving credential:", error);
            throw error;
        }
    },

    async updateCredentialCounter(credentialId: string, newCounter: number) {
        try {
            await promisePool.query(
                "UPDATE credentials SET counter = ? WHERE credential_id = ?",
                [newCounter, credentialId],
            );
        } catch (error) {
            console.error("Error updating credential counter:", error);
            throw error;
        }
    },
};
```

### 5.5 ミドルウェア

エラーを一元的に処理し、デバッグを容易にするために、`errorHandler.ts`ファイルを追加します：

```ts filename="errorHandler.ts"
import { Request, Response, NextFunction } from "express";
import { CustomError } from "./customError";

interface ErrorWithStatus extends Error {
    statusCode?: number;
}

export const handleError = (
    err: CustomError,
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const statusCode = err.statusCode || 500;
    const message = err.message || "Internal Server Error";
    console.log(message);
    res.status(statusCode).send({ error: message });
};
```

さらに、後でバグを迅速に見つけるのに役立つカスタムエラーを作成できるように、新しい`customError.ts`ファイルを追加します：

```ts filename="customError.ts"
export class CustomError extends Error {
    statusCode: number;

    constructor(message: string, statusCode: number = 500) {
        super(message);
        this.statusCode = statusCode;
        Object.setPrototypeOf(this, CustomError.prototype);
    }
}
```

### 5.6 ユーティリティ

`utils`フォルダに、`constants.ts`と`utils.ts`の2つのファイルを作成します。

`constant.ts`には、リライングパーティ名、リライングパーティID、オリジンなど、いくつかの基本的なWebAuthnサーバー情報が保持されます：

```ts filename="constant.ts"
export const rpName: string = "Passkeys Tutorial";
export const rpID: string = "localhost";
export const origin: string = `http://${rpID}:8080`;
```

`utils.ts`には、後でデータのエンコードとデコードに必要な2つの関数が保持されます：

```ts filename="utils.ts"
export const uint8ArrayToBase64 = (uint8Array: Uint8Array): string =>
    Buffer.from(uint8Array).toString("base64");

export const base64ToUint8Array = (base64: string): Uint8Array =>
    new Uint8Array(Buffer.from(base64, "base64"));
```

### 5.7 SimpleWebAuthnを使用したパスキーコントローラー

さて、バックエンドの心臓部であるコントローラーに移ります。新しいパスキーを作成するためのコントローラー（`registration.ts`）と、パスキーでログインするためのコントローラー（`authentication.ts`）の2つを作成します。

`registration.ts`は次のようになります：

```ts filename="registration.ts"
import {
    generateRegistrationOptions,
    verifyRegistrationResponse,
} from "@simplewebauthn/server";
import { uint8ArrayToBase64 } from "../utils/utils";
import { rpName, rpID, origin } from "../utils/constants";
import { credentialService } from "../services/credentialService";
import { userService } from "../services/userService";
import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types";
import { Request, Response, NextFunction } from "express";
import { CustomError } from "../middleware/customError";

export const handleRegisterStart = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { username } = req.body;

    if (!username) {
        return next(new CustomError("Username empty", 400));
    }

    try {
        let user = await userService.getUserByUsername(username);
        if (user) {
            return next(new CustomError("User already exists", 400));
        } else {
            user = await userService.createUser(username);
        }

        const options = await generateRegistrationOptions({
            rpName,
            rpID,
            userID: user.id,
            userName: user.username,
            timeout: 60000,
            attestationType: "direct",
            excludeCredentials: [],
            authenticatorSelection: {
                residentKey: "preferred",
            },
            // Support for the two most common algorithms: ES256, and RS256
            supportedAlgorithmIDs: [-7, -257],
        });
        req.session.loggedInUserId = user.id;
        req.session.currentChallenge = options.challenge;
        res.send(options);
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    }
};

export const handleRegisterFinish = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { body } = req;
    const { currentChallenge, loggedInUserId } = req.session;

    if (!loggedInUserId) {
        return next(new CustomError("User ID is missing", 400));
    }

    if (!currentChallenge) {
        return next(new CustomError("Current challenge is missing", 400));
    }

    try {
        const verification = await verifyRegistrationResponse({
            response: body as RegistrationResponseJSON,
            expectedChallenge: currentChallenge,
            expectedOrigin: origin,
            expectedRPID: rpID,
            requireUserVerification: true,
        });

        if (verification.verified && verification.registrationInfo) {
            const { credentialPublicKey, credentialID, counter } =
                verification.registrationInfo;
            await credentialService.saveNewCredential(
                loggedInUserId,
                uint8ArrayToBase64(credentialID),
                uint8ArrayToBase64(credentialPublicKey),
                counter,
                body.response.transports,
            );
            res.send({ verified: true });
        } else {
            next(new CustomError("Verification failed", 400));
        }
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    } finally {
        req.session.loggedInUserId = undefined;
        req.session.currentChallenge = undefined;
    }
};
```

WebAuthn登録（サインアップ）プロセスの2つの主要なエンドポイントを処理するコントローラーの機能を確認しましょう。これは、パスワードベースの認証との最大の違いの一つでもあります。登録（サインアップ）または認証（ログイン）の試行ごとに、2つのバックエンドAPI呼び出しが必要となり、その間には特定のフロントエンドコンテンツが必要です。パスワードは通常、1つのエンドポイントしか必要としません。

**1. handleRegisterStart エンドポイント：**

このエンドポイントはフロントエンドによってトリガーされ、新しいパスキーとアカウントを作成するためのユーザー名を受け取ります。この例では、まだアカウントが存在しない場合にのみ新しいアカウント/パスキーの作成を許可します。実際のアプリケーションでは、パスキーがすでに存在し、同じデバイスからの追加は不可能であること（ただし、ユーザーは何らかの確認の後に別のデバイスからパスキーを追加できること）をユーザーに伝える方法でこれを処理する必要があります。簡単にするため、このチュートリアルではこれを無視します。

[PublicKeyCredentialCreationOptions](https://www.corbado.com/glossary/publickeycredentialcreationoptions)が準備されます。`residentKey`はpreferredに設定され、`attestationType`はdirectに設定され、データベースに保存する可能性のあるオーセンティケータからより多くのデータを収集します。

一般的に、[PublicKeyCredentialCreationOptions](https://www.corbado.com/glossary/publickeycredentialcreationoptions)は次のデータで構成されます：

```
dictionary PublicKeyCredentialCreationOptions {
    required PublicKeyCredentialRpEntity                rp;
    required PublicKeyCredentialUserEntity              user;

    required BufferSource                               challenge;
    required sequence<PublicKeyCredentialParameters>    pubKeyCredParams;

    unsigned long                                       timeout;
    sequence<PublicKeyCredentialDescriptor>             excludeCredentials = [];
    AuthenticatorSelectionCriteria                      authenticatorSelection;
    DOMString                                           attestation = "none";
    AuthenticationExtensionsClientInputs                extensions;
};
```

- **rp：**
  リライングパーティ（ウェブサイトまたはサービス）情報を表し、通常はその名前（`rp.name`）とドメイン（`rp.id`）を含みます。
- **user：**
  `user.name`、`user.id`、`user.displayName`などのユーザーアカウントの詳細を含みます。
- **challenge：**
  登録プロセス中のリプレイ攻撃を防ぐためにWebAuthnサーバーによって作成された、安全でランダムな値。
- **pubKeyCredParams：**
  使用される暗号アルゴリズムを含む、作成される公開鍵クレデンシャルのタイプを指定します。
- **timeout：**
  オプションで、ユーザーがインタラクションを完了するまでの時間をミリ秒単位で設定します。
- **excludeCredentials：**
  除外するクレデンシャルのリスト。同じデバイス/オーセンティケータに対して複数回パスキーを登録することを防ぐために使用されます。
- **authenticatorSelection：**
  ユーザー検証をサポートする必要があるか、またはレジデントキーをどのように奨励すべきかなど、オーセンティケータを選択するための基準。
- **attestation：**
  「none」、「indirect」、「direct」など、希望するアテステーション伝達の優先順位を指定します。
- **extensions：** オプションで、追加のクライアント拡張を許可します。

ユーザーIDとチャレンジはセッションオブジェクトに保存され、チュートリアルの目的でプロセスを簡素化します。さらに、各登録（サインアップ）または認証（ログイン）の試行後にセッションはクリアされます。

**2. handleRegisterFinish エンドポイント：**

このエンドポイントは、以前に設定されたユーザーIDとチャレンジを取得します。`RegistrationResponse`をチャレンジで検証します。有効な場合、ユーザーの新しいクレデンシャルを保存します。データベースに保存されると、ユーザーIDとチャレンジはセッションから削除されます。

ヒント：アプリケーションをデバッグする際には、ブラウザとして[Chrome](https://www.corbado.com/ja/blog/digital-credentials-api)を使用し、その組み込み機能（例えば、仮想WebAuthnオーセンティケータやデバイスログなど）を活用して、パスキーベースのアプリケーションの開発者体験を向上させることを強くお勧めします（詳細については、以下の開発者向けのヒントを参照してください）。

次に、同様の構造と機能を持つ`authentication.ts`に移ります。

`authentication.ts`は次のようになります：

```ts filename="authentication.ts"
import { Request, Response, NextFunction } from "express";
import {
    generateAuthenticationOptions,
    verifyAuthenticationResponse,
} from "@simplewebauthn/server";
import { uint8ArrayToBase64, base64ToUint8Array } from "../utils/utils";
import { rpID, origin } from "../utils/constants";
import { credentialService } from "../services/credentialService";
import { userService } from "../services/userService";
import { AuthenticatorDevice } from "@simplewebauthn/typescript-types";
import { isoBase64URL } from "@simplewebauthn/server/helpers";
import {
    VerifiedAuthenticationResponse,
    VerifyAuthenticationResponseOpts,
} from "@simplewebauthn/server/esm";
import { CustomError } from "../middleware/customError";

export const handleLoginStart = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { username } = req.body;
    try {
        const user = await userService.getUserByUsername(username);
        if (!user) {
            return next(new CustomError("User not found", 404));
        }

        req.session.loggedInUserId = user.id;

        // allowCredentials is purposely for this demo left empty. This causes all existing local credentials
        // to be displayed for the service instead only the ones the username has registered.
        const options = await generateAuthenticationOptions({
            timeout: 60000,
            allowCredentials: [],
            userVerification: "required",
            rpID,
        });

        req.session.currentChallenge = options.challenge;
        res.send(options);
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    }
};

export const handleLoginFinish = async (
    req: Request,
    res: Response,
    next: NextFunction,
) => {
    const { body } = req;
    const { currentChallenge, loggedInUserId } = req.session;

    if (!loggedInUserId) {
        return next(new CustomError("User ID is missing", 400));
    }

    if (!currentChallenge) {
        return next(new CustomError("Current challenge is missing", 400));
    }

    try {
        const credentialID = isoBase64URL.toBase64(body.rawId);
        const bodyCredIDBuffer = isoBase64URL.toBuffer(body.rawId);
        const dbCredential: AuthenticatorDevice | null =
            await credentialService.getCredentialByCredentialId(credentialID);
        if (!dbCredential) {
            return next(new CustomError("Credential not registered with this site", 404));
        }

        // @ts-ignore
        const user = await userService.getUserById(dbCredential.userID);
        if (!user) {
            return next(new CustomError("User not found", 404));
        }

        // @ts-ignore
        dbCredential.credentialID = base64ToUint8Array(dbCredential.credentialID);
        // @ts-ignore
        dbCredential.credentialPublicKey = base64ToUint8Array(
            dbCredential.credentialPublicKey,
        );

        let verification: VerifiedAuthenticationResponse;
        const opts: VerifyAuthenticationResponseOpts = {
            response: body,
            expectedChallenge: currentChallenge,
            expectedOrigin: origin,
            expectedRPID: rpID,
            authenticator: dbCredential,
        };
        verification = await verifyAuthenticationResponse(opts);
        const { verified, authenticationInfo } = verification;

        if (verified) {
            await credentialService.updateCredentialCounter(
                uint8ArrayToBase64(bodyCredIDBuffer),
                authenticationInfo.newCounter,
            );
            res.send({ verified: true });
        } else {
            next(new CustomError("Verification failed", 400));
        }
    } catch (error) {
        next(
            error instanceof CustomError
                ? error
                : new CustomError("Internal Server Error", 500),
        );
    } finally {
        req.session.currentChallenge = undefined;
        req.session.loggedInUserId = undefined;
    }
};
```

私たちの認証（ログイン）プロセスには2つのエンドポイントが含まれます：

**1. handleLoginStart エンドポイント：**

このエンドポイントは、ユーザーがログインしようとするとアクティブになります。まず、ユーザー名がデータベースに存在するかどうかを確認し、見つからない場合はエラーを返します。実際のシナリオでは、代わりに新しいアカウントを作成することを提案するかもしれません。

既存のユーザーの場合、データベースからユーザーIDを取得し、セッションに保存し、[PublicKeyCredentialRequestOptions](https://www.corbado.com/glossary/publickeycredentialrequestoptions)オプションを生成します。`allowCredentials`は、クレデンシャルの使用を制限しないように空のままにしています。そのため、このリライングパーティで利用可能なすべてのパスキーがパスキーモーダルで選択できます。

生成されたチャレンジもセッションに保存され、[PublicKeyCredentialRequestOptions](https://www.corbado.com/glossary/publickeycredentialrequestoptions)はフロントエンドに返送されます。

[PublicKeyCredentialRequestOptions](https://www.corbado.com/glossary/publickeycredentialrequestoptions)は次のデータで構成されます：

```
dictionary PublicKeyCredentialRequestOptions {
    required BufferSource                challenge;
    unsigned long                        timeout;
    USVString                            rpId;
    sequence<PublicKeyCredentialDescriptor> allowCredentials = [];
    DOMString                            userVerification = "preferred";
    AuthenticationExtensionsClientInputs extensions;
};
```

- **challenge：**
  認証プロセス中のリプレイ攻撃を防ぐためにWebAuthnサーバーから提供される、安全でランダムな値。
- **timeout：**
  オプションで、ユーザーが認証リクエストに応答するまでの時間をミリ秒単位で設定します。
- **rpId：** リライングパーティIDで、通常はサービスのドメインです。
- **allowCredentials：**
  オプションのクレデンシャル記述子のリストで、この認証（ログイン）に使用できるクレデンシャルを指定します。
- **userVerification：**
  「required」、「preferred」、「discouraged」など、ユーザー検証の要件を指定します。
- **extensions：** オプションで、追加のクライアント拡張を許可します。

**2. handleLoginFinish エンドポイント：**

このエンドポイントは、セッションから`currentChallenge`と`loggedInUserId`を取得します。

ボディからのクレデンシャルIDを使用して、データベースで正しいクレデンシャルをクエリします。クレデンシャルが見つかった場合、このクレデンシャルIDに関連付けられたユーザーが認証（ログイン）できることを意味します。次に、クレデンシャルから取得したユーザーIDを介してユーザーテーブルからユーザーをクエリし、チャレンジとリクエストボディを使用して`authenticationResponse`を検証できます。すべてが成功した場合、ログイン成功メッセージを表示します。一致するクレデンシャルが見つからない場合は、エラーが送信されます。

さらに、検証が成功した場合、クレデンシャルのカウンターが更新され、使用されたチャレンジとloggedInUserIdがセッションから削除されます。

その上で、`src/app`と`src/constant`フォルダを、そこにあるすべてのファイルと共に削除できます。

注意：実際のアプリケーションでは重要な適切なセッション管理とルート保護は、このチュートリアルでは簡単にするために省略されています。

### 5.8 パスキールート

最後に、コントローラーが到達可能であることを確認するために、新しいディレクトリ`src/routes`にある`routes.ts`に適切なルートを追加する必要があります：

```ts filename="routes.ts"
import express from "express";
import { handleError } from "../middleware/errorHandler";
import { handleRegisterStart, handleRegisterFinish } from "../controllers/registration";
import { handleLoginStart, handleLoginFinish } from "../controllers/authentication";

const router = express.Router();

router.post("/registerStart", handleRegisterStart);
router.post("/registerFinish", handleRegisterFinish);
router.post("/loginStart", handleLoginStart);
router.post("/loginFinish", handleLoginFinish);

router.use(handleError);

export { router };
```

## 6. フロントエンドにパスキーを統合する

このパスキーチュートリアルの部分では、アプリケーションのフロントエンドでパスキーをサポートする方法に焦点を当てます。私たちは、`index.html`、`styles.css`、`script.js`の3つのファイルからなる非常に基本的なフロントエンドを持っています。これら3つのファイルはすべて、新しい`src/public`フォルダにあります。

`index.html`ファイルには、ユーザー名の入力フィールドと、登録およびログインするための2つのボタンが含まれています。さらに、`js/script.js`ファイルでブラウザのWeb
Authentication
APIとの対話を簡素化する`@simplewebauthn/browser`スクリプトをインポートします。

`index.html`は次のようになります：

```html filename="index.html"
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Passkey Tutorial</title>
        <link rel="stylesheet" href="css/style.css" />
    </head>
    <body>
        <div class="container">
            <h1>Passkey Tutorial</h1>
            <div id="message"></div>

            <div class="input-group">
                <input type="text" id="username" placeholder="Enter username" />
                <button id="registerButton">Register</button>
                <button id="loginButton">Login</button>
            </div>
        </div>

        <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script>
        <script src="js/script.js"></script>
    </body>
</html>
```

`script.js`は次のようになります：

```javascript filename="script.js"
document.getElementById("registerButton").addEventListener("click", register);
document.getElementById("loginButton").addEventListener("click", login);

function showMessage(message, isError = false) {
    const messageElement = document.getElementById("message");
    messageElement.textContent = message;
    messageElement.style.color = isError ? "red" : "green";
}

async function register() {
    // Retrieve the username from the input field
    const username = document.getElementById("username").value;

    try {
        // Get registration options from your server. Here, we also receive the challenge.
        const response = await fetch("/api/passkey/registerStart", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ username: username }),
        });
        console.log(response);

        // Check if the registration options are ok.
        if (!response.ok) {
            throw new Error(
                "User already exists or failed to get registration options from server",
            );
        }

        // Convert the registration options to JSON.
        const options = await response.json();
        console.log(options);

        // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
        // A new attestation is created. This also means a new public-private-key pair is created.
        const attestationResponse =
            await SimpleWebAuthnBrowser.startRegistration(options);

        // Send attestationResponse back to server for verification and storage.
        const verificationResponse = await fetch("/api/passkey/registerFinish", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(attestationResponse),
        });

        if (verificationResponse.ok) {
            showMessage("Registration successful");
        } else {
            showMessage("Registration failed", true);
        }
    } catch (error) {
        showMessage("Error: " + error.message, true);
    }
}

async function login() {
    // Retrieve the username from the input field
    const username = document.getElementById("username").value;

    try {
        // Get login options from your server. Here, we also receive the challenge.
        const response = await fetch("/api/passkey/loginStart", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ username: username }),
        });
        // Check if the login options are ok.
        if (!response.ok) {
            throw new Error("Failed to get login options from server");
        }
        // Convert the login options to JSON.
        const options = await response.json();
        console.log(options);

        // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
        // A new assertionResponse is created. This also means that the challenge has been signed.
        const assertionResponse =
            await SimpleWebAuthnBrowser.startAuthentication(options);

        // Send assertionResponse back to server for verification.
        const verificationResponse = await fetch("/api/passkey/loginFinish", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(assertionResponse),
        });

        if (verificationResponse.ok) {
            showMessage("Login successful");
        } else {
            showMessage("Login failed", true);
        }
    } catch (error) {
        showMessage("Error: " + error.message, true);
    }
}
```

`script.js`には、3つの主要な関数があります：

**1. showMessage 関数：**

これは主にエラーメッセージを表示するために使用されるユーティリティ関数で、デバッグに役立ちます。

**2. Register 関数：**

ユーザーが「Register」をクリックするとトリガーされます。入力フィールドからユーザー名を取得し、passkeyRegisterStartエンドポイントに送信します。レスポンスには**PublicKeyCredentialCreationOptions**が含まれており、これらはJSONに変換されて`SimpleWebAuthnBrowser.startRegistration`に渡されます。この呼び出しにより、デバイスのオーセンティケータ（Face
IDやTouch
IDなど）がアクティブになります。ローカル認証が成功すると、署名されたチャレンジが`passkeyRegisterFinish`エンドポイントに返送され、パスキー作成プロセスが完了します。

登録（サインアップ）プロセス中、`attestation`オブジェクトは重要な役割を果たしますので、詳しく見てみましょう。

![Passkey Attestation Object](https://www.corbado.com/website-assets/6572cda3f3869adee2c38e05_passkey_attestation_object_5bfcce1977.png)

`attestation`オブジェクトは、主に`fmt`、`attStmt`、`authData`の3つのコンポーネントで構成されます。`fmt`要素はアテステーションステートメントのフォーマットを示し、`attStmt`は実際のアテステーションステートメント自体を表します。アテステーションが不要と見なされるシナリオでは、`fmt`は「none」に指定され、`attStmt`は空になります。

この構造の中で焦点となるのは`authData`セグメントです。このセグメントは、リライングパーティID、フラグ、カウンター、証明済みクレデンシャルデータなどの重要な要素をサーバーで取得するための鍵となります。フラグに関しては、特に興味深いのはBS（バックアップ状態）とBE（バックアップ適格性）で、これらはパスキーが同期されているかどうか（例えば、iCloudキーチェーンや[1Password](https://www.corbado.com/blog/1password-passkeys-best-practices-analysis)経由で）に関する詳細情報を提供します。さらに、UV（ユーザー検証）とUP（ユーザープレゼンス）も有用な情報を提供します。

![Passkey Authenticator Data](https://www.corbado.com/website-assets/6572cf3ce1fa2982352d6c88_passkey_authenticator_data_7f61aceaea.png)

アテステーションオブジェクトのさまざまな部分（オーセンティケータデータ、リライングパーティID、アテステーションステートメントなど）は、オーセンティケータによってその秘密鍵を使用してハッシュ化またはデジタル署名されることに注意することが重要です。このプロセスは、アテステーションオブジェクト全体の完全性を維持するために不可欠です。

**3. Login 関数：**

ユーザーが「Login」をクリックするとアクティブになります。登録関数と同様に、ユーザー名を取得して`passkeyLoginStart`エンドポイントに送信します。レスポンスには**PublicKeyCredentialRequestOptions**が含まれており、JSONに変換されて`SimpleWebAuthnBrowser.startAuthentication`で使用されます。これにより、デバイスでのローカル認証がトリガーされます。署名されたチャレンジは`passkeyLoginFinish`エンドポイントに返送されます。このエンドポイントからの成功レスポンスは、ユーザーがアプリに正常にログインしたことを示します。

さらに、付属のCSSファイルはアプリケーションにシンプルなスタイリングを提供します：

```css
body {
    font-family: "Helvetica Neue", Arial, sans-serif;
    text-align: center;
    padding: 40px;
    background-color: #f3f4f6;
    color: #333;
}

.container {
    max-width: 400px;
    margin: auto;
    background: white;
    padding: 20px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    border-radius: 8px;
}

h1 {
    color: #007bff;
    font-size: 24px;
    margin-bottom: 20px;
}

.input-group {
    margin-bottom: 20px;
}

input[type="text"] {
    padding: 10px;
    margin-bottom: 10px;
    border: 1px solid #ced4da;
    border-radius: 4px;
    width: calc(100% - 22px);
}

button {
    width: calc(50% - 20px);
    padding: 10px 0;
    margin: 5px;
    font-size: 16px;
    cursor: pointer;
    border: none;
    border-radius: 4px;
    background-color: #007bff;
    color: white;
}

button:hover {
    background-color: #0056b3;
}

#message {
    color: #dc3545;
    margin: 20px;
}
```

## 7. パスキーサンプルアプリの実行

アプリケーションを実際に動作させるには、TypeScriptコードをコンパイルして実行します：

```bash
npm run dev
```

これでサーバーが[http://localhost:8080](http://localhost:8080)で起動しているはずです。

**本番環境に関する考慮事項：**

私たちがカバーしたのは基本的な概要であることを忘れないでください。本番環境でパスキーアプリケーションをデプロイする場合、以下についてさらに深く掘り下げる必要があります：

- **セキュリティ対策：**
  ユーザーデータを保護するための堅牢なセキュリティ対策を実装します。
- **エラー処理：** アプリケーションがエラーを適切に処理し、ログに記録するようにします。
- **データベース管理：** スケーラビリティと信頼性のためにデータベース操作を最適化します。

## 8. パスキーのDevOps統合

すでにデータベース用のDockerコンテナをセットアップしました。次に、Docker
Composeのセットアップを拡張して、バックエンドとフロントエンドの両方を含むサーバーを追加します。`docker-compose.yml`ファイルを適宜更新する必要があります。

アプリケーションをコンテナ化するために、必要なパッケージをインストールし、開発サーバーを起動する新しいDockerfileを作成します：

```docker filename="Docker"
# Use an official Node runtime as a parent image
FROM node:20-alpine

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install any needed packages
RUN npm install

# Bundle your app's source code inside the Docker image
COPY . .

# Make port 8080 available to the world outside this container
EXPOSE 8080

# Define the command to run your app
CMD ["npm", "run", "dev"]
```

次に、このコンテナを起動するために`docker-compose.yml`ファイルも拡張します：

```yaml filename="docker-compose.yml"
version: "3.1"

services:
    db:
        image: mysql
        command: --default-authentication-plugin=mysql_native_password
        restart: always
        environment:
            MYSQL_ROOT_PASSWORD: my-secret-pw
            MYSQL_DATABASE: webauthn_db
        ports:
            - "3306:3306"
        volumes:
            - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql

    app:
        build: .
        ports:
            - "8080:8080"
        environment:
            - DB_HOST=db
            - DB_USER=root
            - DB_PASSWORD=my-secret-pw
            - DB_NAME=webauthn_db
            - SESSION_SECRET=secret123
        depends_on:
            - db
```

ターミナルで `docker compose up`
を実行し、[http://localhost:8080](http://localhost:8080)にアクセスすると、パスキーWebアプリの動作バージョンが表示されるはずです（ここでは[Windows 11](https://www.corbado.com/blog/passkeys-windows-11)
23H2 + [Chrome](https://www.corbado.com/ja/blog/digital-credentials-api) 119で実行しています）：

![Passkey Tutorial Windows Hello](https://www.corbado.com/website-assets/6572cf57f3869adee2c49ff5_passkey_tutorial_windows_hello_5ac0585957.png)

## 9. 開発者向けの追加のパスキーのヒント

私たちはかなりの期間パスキーの実装に取り組んできたので、実際のパスキーアプリで作業する場合、いくつかの課題に遭遇しました：

- デバイス/プラットフォームの互換性とサポート
- ユーザーのオンボーディングと教育
- 紛失または変更されたデバイスの処理
- クロスプラットフォーム認証
- フォールバックメカニズム
- エンコーディングの複雑さ：エンコーディングはしばしば最も難しい部分です。JSON、[CBOR](https://www.corbado.com/glossary/cbor)、uint8array、バッファ、ブロブ、さまざまなデータベース、base64、base64urlを扱う必要があり、多くのエラーが発生する可能性があります。
- パスキー管理（例：パスキーの追加、削除、名前変更）

さらに、実装部分に関して開発者向けの以下のヒントがあります：

**Passkeys Debuggerを活用する**

[**Passkeys debugger**](https://www.passkeys-debugger.io/)は、さまざまなWebAuthnサーバー設定とクライアント応答をテストするのに役立ちます。さらに、オーセンティケータ応答のための優れたパーサーを提供します。

**Chromeのデバイスログ機能でデバッグする**

Chromeのデバイスログ（[chrome://device-log/](chrome://device-log/)経由でアクセス可能）を使用して、[FIDO](https://www.corbado.com/ja/blog/emv-3ds-acs-passkeys-fido-spc-katsuyou)/WebAuthn呼び出しを監視します。この機能は、認証（ログイン）プロセスのリアルタイムログを提供し、交換されているデータを確認し、発生した問題をトラブルシューティングすることができます。

Chromeですべてのパスキーを取得するためのもう1つの非常に便利なショートカットは、[chrome://settings/passkeys](chrome://settings/passkeys)を使用することです。

**Chromeの仮想WebAuthnオーセンティケータを使用する**

開発中にTouch ID、Face
ID、または[Windows Hello](https://www.corbado.com/glossary/windows-hello)プロンプトを使用するのを避けるために、Chromeには実際のオーセンティケータをエミュレートする非常に便利な仮想WebAuthnオーセンティケータが付属しています。作業をスピードアップするために使用することを強くお勧めします。詳細は[こちら](https://developer.chrome.com/docs/devtools/webauthn/)をご覧ください。

**異なるプラットフォームとブラウザでテストする**

さまざまなブラウザとプラットフォームでの互換性と機能性を確認してください。WebAuthnはブラウザによって動作が異なるため、徹底的なテストが鍵となります。

**異なるデバイスでテストする**

ここでは、[ngrok](https://www.corbado.com/blog/multi-device-passkey-login-corbado-ngrok)のようなツールを使用して、ローカルアプリケーションを他の（モバイル）デバイスで到達可能にすることが特に便利です。

**ユーザー検証をPreferredに設定する**

**PublicKeyCredentialRequestOptions**で[userVerification](https://www.corbado.com/glossary/user-verification)のプロパティを定義する際、使いやすさとセキュリティの良いトレードオフであるため、preferredに設定することを選択します。これは、適切なデバイスではセキュリティチェックが実施され、生体認証機能のないデバイスではユーザーフレンドリーさが保たれることを意味します。

## 10. 結論：パスキーチュートリアル

このパスキーチュートリアルが、パスキーを効果的に実装する方法についての明確な理解を提供できたことを願っています。チュートリアルを通して、基本的な概念と実践的な実装に焦点を当て、パスキーアプリケーションを作成するための重要なステップを歩んできました。このガイドは出発点として役立ちますが、WebAuthnの世界には探求し、洗練させるべきことがまだまだたくさんあります。

開発者の皆様には、パスキーのニュアンス（例えば、複数のパスキーの追加、デバイスのパスキー対応状況の確認、回復ソリューションの提供など）にさらに深く飛び込むことをお勧めします。これは、ユーザー認証を強化する上で挑戦と大きな報酬の両方を提供する、価値のある旅です。パスキーを使えば、単に機能を構築するだけでなく、より安全でユーザーフレンドリーなデジタル世界に貢献することになります。
