Get your free and exclusive 80-page Banking Passkey Report
passkey tutorial how to implement passkeys

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

このチュートリアルでは、Webアプリにパスキーを実装する方法を解説します。Node.js (TypeScript)、SimpleWebAuthn、Vanilla HTML / JavaScript、MySQLを使用します。

Vincent Delitz

Vincent

Created: June 17, 2025

Updated: June 20, 2025


We aim to make the Internet a safer place using passkeys. That's why we want to support developers with tutorials on how to implement passkeys.

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

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

Demo Icon

Want to try passkeys yourself in a passkeys demo?

Try Passkeys

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

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

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

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

PasskeyAssessment Icon

Get a free passkey assessment in 15 minutes.

Book free consultation

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

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

お急ぎの方やリファレンスが必要な方のために、コードベース全体はGitHubで公開しています。

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

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

StateOfPasskeys Icon

Want to find out how many people use passkeys?

View Adoption Data

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

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

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

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

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

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

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

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

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

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

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

2.3 データベース:MySQL#

すべてのユーザーデータとパスキーの公開鍵はデータベースに保存されます。データベース技術としてMySQLを選択しました。MySQLとリレーショナルデータベースの基礎的な理解があると役立ちますが、個々のステップをガイドします。

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

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

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We’ve written 150+ blog posts on passkeys.

Join Passkeys Community

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

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

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

Are your users passkey-ready?

Test Passkey-Readiness

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

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

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

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

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が authenticatorauthenticator_device と名付けることを提案しているのとは対照的です)。

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

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

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

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 ファイルを作成します:

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ファイルにはこの単一のコンテナしか含まれていませんが、後でさらにコンポーネントを追加します。コンテナを起動するには、次のコマンドを使用します:

docker compose up -d

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

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

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

use webauthn_db; show tables;

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

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

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

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

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

コマンドを実行します

npx create-express-typescript-application passkeys-tutorial

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

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

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

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

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

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

npm run dev:nodemon

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

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

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

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

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

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

5.2 MySQLデータベース接続#

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

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つの変数がすでに定義されています:

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

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

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は次のようになります:

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が機能するようにします。さらに、ルーティングを合理化し、エラー処理を一旦削除します(これは後でミドルウェアに追加されます):

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.tsuserService.tsを開発します。

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

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

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は次のようになります:

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ファイルを追加します:

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ファイルを追加します:

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.tsutils.tsの2つのファイルを作成します。

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

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

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

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は次のようになります:

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が準備されます。residentKeyはpreferredに設定され、attestationTypeはdirectに設定され、データベースに保存する可能性のあるオーセンティケータからより多くのデータを収集します。

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

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

2. handleRegisterFinish エンドポイント:

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

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

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

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

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オプションを生成します。allowCredentialsは、クレデンシャルの使用を制限しないように空のままにしています。そのため、このリライングパーティで利用可能なすべてのパスキーがパスキーモーダルで選択できます。

生成されたチャレンジもセッションに保存され、PublicKeyCredentialRequestOptionsはフロントエンドに返送されます。

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 エンドポイント:

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

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

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

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

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

5.8 パスキールート#

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

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 };
Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

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

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

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

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

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は次のようになります:

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オブジェクトは重要な役割を果たしますので、詳しく見てみましょう。

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

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

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

3. Login 関数:

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

さらに、付属の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コードをコンパイルして実行します:

npm run dev

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

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

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

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

8. パスキーのDevOps統合#

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

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

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ファイルも拡張します:

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にアクセスすると、パスキーWebアプリの動作バージョンが表示されるはずです(ここではWindows 11 23H2 + Chrome 119で実行しています):

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

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

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

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

Passkeys Debuggerを活用する

Passkeys debuggerは、さまざまなWebAuthnサーバー設定とクライアント応答をテストするのに役立ちます。さらに、オーセンティケータ応答のための優れたパーサーを提供します。

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

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

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

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

開発中にTouch ID、Face ID、またはWindows Helloプロンプトを使用するのを避けるために、Chromeには実際のオーセンティケータをエミュレートする非常に便利な仮想WebAuthnオーセンティケータが付属しています。作業をスピードアップするために使用することを強くお勧めします。詳細はこちらをご覧ください。

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

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

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

ここでは、ngrokのようなツールを使用して、ローカルアプリケーションを他の(モバイル)デバイスで到達可能にすることが特に便利です。

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

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

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

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

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

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.

Related Articles