コンテンツにスキップ

Claude CodeでWebアプリに管理画面を追加してパスキー認証でアクセス制限する

⚠️ このハンズオンは制作途中です。実際に動かして検証しながら手順を確定させている段階で、説明やプロンプトが今後書き換わる可能性があります。気になる点があればフィードバックいただけると助かります。

このハンズオンはGit連携版またはGitHub Actions版(アーカイブ)のD1ハンズオンの続編。前回作った匿名一行掲示板に、管理者だけが使える管理画面を追加し、パスキー(WebAuthn)でアクセス制限する。合言葉(TKN)でも外部サービス(ADM)でもなく、端末の指紋・顔・PIN でログインするパスワードレス認証を、自前で(SimpleWebAuthn + D1 で)組む。

DBを使うWebアプリには、DB管理という問題が付きまとう。掲示板なら「不適切な投稿を削除したい」「スパムを非表示にしたい」といった操作が必要になる。そのたびに SQL を直接叩くのは手間だし、ミスのリスクもある。

こうした管理操作はWebアプリの中に管理画面として用意しておくのが定石。必要な操作をボタンひとつでできるようにしておけば、後の運用がぐっと楽になる。

ただし、管理画面は誰でもアクセスできてはまずい。一般ユーザーが投稿を操作できてしまう。特定の人(管理者)だけが使えるように制限する必要がある。

ということで、このハンズオンでは、前回作った掲示板に以下を追加する。

  • 管理画面/admin):投稿の表示/非表示を切り替えられる
  • パスキー認証/api/admin 配下を、登録したパスキーでログインした人だけが使えるように制限する

掲示板

前回作った掲示板

管理画面

今回作る管理画面

1-2. パスキーとは——合言葉のない認証

Section titled “1-2. パスキーとは——合言葉のない認証”

TKN の合言葉方式(Basic認証・トークン認証)は「サーバーに置いた合言葉を、送られてきた値と照合するだけ」だった。手軽だが、合言葉そのものが漏れたら終わり(誰が使ったかも分からない)という弱点がある。

パスキーは、その合言葉自体をなくす。仕組みの正体は WebAuthn というブラウザ標準で、要点は「秘密の値を送らない」こと。

  • 登録時、端末(スマホ・PC)の中で鍵ペア(秘密鍵・公開鍵)が作られる。公開鍵だけがサーバーに保存され、秘密鍵は端末から一切出ない
  • ログイン時は、サーバーが出す使い捨てのチャレンジに端末が署名し、サーバーは保存済みの公開鍵で署名を検証する。パスワードもトークンもネットワークを流れない
  • 署名できるのは、端末のロック解除(指紋・顔・PIN)を通った人だけ

このため、パスキーには合言葉方式に無い性質がある。

  • 漏れる秘密がない:サーバーが保持するのは公開鍵だけ。DBが漏れてもログインはできない
  • フィッシングに強い:パスキーは登録したドメインに束縛される。偽サイトでは署名が成立しない
  • パスワードレス:覚える・入力する合言葉がない。端末の生体認証だけ
  • 誰のパスキーかを識別できる:どのパスキーでログインしたか分かる(合言葉方式は「知っているか」しか分からない)

秘密鍵は端末から出ないが、同じ人の端末では共有される: iCloudキーチェーンや 1Password などの同期パスキーなら、同じ Apple ID の iPhone と Mac は同じパスキーを共有する(iCloud 経由で同期されるため)。片方で登録すればもう片方でもそのまま使え、「端末ごとにバラバラ」ではない。この同期まわりは 6-3 で詳しく扱う。

このハンズオンの立ち位置: パスキーは本来「一般ユーザーのログイン機能」にも使える本格的な認証。ただし会員登録・複数ユーザー管理まで作り込むと一気に大きくなるので、ここでは TKNADM と同じ「自分(管理者)だけが入れる管理画面」を題材に、パスキーの一番小さい形を体験する。

1-3. なぜ「自前(SimpleWebAuthn + D1)」なのか

Section titled “1-3. なぜ「自前(SimpleWebAuthn + D1)」なのか”

パスキーは Hanko や Clerk などのホスト型サービスを使えばボタン1つで載せられる。それでも本ハンズオンで自前実装を選ぶのは、本サイトのスタンス(裏側を理解する・外部サービスに依存しない・Cloudflare 内で完結)に沿うため。

  • SimpleWebAuthn:WebAuthn の難所(チャレンジ生成・署名検証)を引き受けてくれるライブラリ。サーバー側(@simplewebauthn/server)とブラウザ側(@simplewebauthn/browser)がある
  • 保存先は D1:公開鍵・チャレンジ・セッションを、前回作った掲示板と同じ D1 に置く
  • 動く場所は Pages Functions:Workers に作り替える必要はない。Pages Functions は中身が Workers ランタイムなので、@simplewebauthn/server がそのまま動く

これで TKN/ADM とまったく同じスタック(Cloudflare Pages + Pages Functions + D1)のまま、認証の中身だけパスキーに差し替える形になる。

このハンズオンでやることの全体像。

  1. ライブラリの追加(Claude Code)
    • @simplewebauthn/server を依存に追加
  2. D1 スキーマ変更(Claude Code)
    • posts に hidden カラムを追加/パスキー用テーブル(認証情報・チャレンジ・セッション)を追加
  3. バックエンド追加(Claude Code)
    • 管理用 API + パスキーの登録・ログイン API + セッション検問のミドルウェア
  4. フロントエンド追加(Claude Code)
    • /admin ページ(パスキー登録・ログイン UI + 投稿一覧)
  5. デプロイ → 最初のパスキーを登録 → 動作確認
  6. どの方式を選ぶか(TKN/ADM との比較)

実装はすべて Claude Code にプロンプトで依頼できる。ブラウザでの手作業は最初のパスキー登録と動作確認だけ。

前提:Claude Codeの作業フォルダは前回のハンズオン(Git連携版/GitHub Actions版)で作った ~/Desktop/claude/my-bbs

パスキーの署名検証を担う @simplewebauthn/server を、プロジェクトの依存に追加する。

Claude
このプロジェクトの Pages Functions から @simplewebauthn/server を使いたい。
package.json が無ければ作って、@simplewebauthn/server を dependencies に追加して npm install して。

これで functions/ から import { ... } from "@simplewebauthn/server" で使える(検証時のバージョンは @simplewebauthn/server@13)。あわせて、本番デプロイで必ずハマる2点を先に潰しておく

nodejs_compat を有効化する(必須): 公開鍵を base64 にする処理などで Node の Buffer を使うコードになりがちで、Cloudflare の Workers ランタイムには Buffer が標準では無い。wrangler.jsonc に次を足す(compatibility_date は 2024-09-23 以降)。

"compatibility_flags": ["nodejs_compat"],

これが無いと、ビルドは通るのに登録/ログインの実行時に 500Buffer is not defined)になる。気づきにくいので先に入れておく。

② Cloudflare のビルドで npm install を走らせる(必須): Pages Functions が npm パッケージを使う場合、Cloudflare 側のビルドで依存を入れないと Functions のバンドルに失敗してデプロイが落ちる。Git連携のプロジェクトは、Cloudflare ダッシュボードの Settings → BuildBuild command を npm install に設定しておく(D1G では静的サイトなので空欄だった)。詳しくは 6章で再掲。

ブラウザ側(@simplewebauthn/browser)は、ビルドを増やさないよう管理画面の HTML から ESM CDN で直接読み込む(5章)。サーバ側とメジャー版を揃えること(サーバが v13 なら @simplewebauthn/browser@13)。

3. 【データベース】スキーマ変更

Section titled “3. 【データベース】スキーマ変更”

今回はテーブルを2種類いじる。(A) 管理画面のための hidden カラム(TKN/ADM と同じ)と、(B) パスキー認証のためのテーブル

「投稿を非表示にする」機能を実装するには、投稿ごとの「表示/非表示」状態をDBに保存する必要がある。Claude Code に「投稿を非表示にできる管理画面を作りたい」と相談すると、hidden カラムの追加が提案される。ここではその手順を先に進める。

マイグレーションファイルの作成を Claude Code に依頼する。

Claude
postsテーブルにhiddenカラム(INTEGER型、デフォルト0)を追加するマイグレーションファイルを作成して。ファイル名はmigrations/0002_add_hidden.sqlで

作成されたファイルを確認する。内容はこのようになっているはず。

ALTER TABLE posts ADD COLUMN hidden INTEGER NOT NULL DEFAULT 0;

パスキー認証には3つのものを保存する必要がある。

  • 認証情報(公開鍵):登録したパスキーごとに、credential ID・公開鍵・署名カウンタを保存する
  • チャレンジ:登録/ログインは「①チャレンジ発行 → ②署名を検証」の2リクエストに分かれる。その間、発行したチャレンジを覚えておく必要がある(Workers はステートレスなのでメモリには置けない)
  • セッション:ログイン成功後に発行する「入館証」。以降の管理API はこれで通す

Claude Code に依頼する。

Claude
パスキー認証用のマイグレーションファイル migrations/0003_add_passkey.sql を作成して。テーブルは3つ。
1. admin_credentials … 登録したパスキー。列: id(TEXT, PK), credential_id(TEXT, UNIQUE), public_key(TEXT), counter(INTEGER), transports(TEXT), created_at(TEXT)
2. admin_challenges … 発行中のチャレンジ。列: id(TEXT, PK), challenge(TEXT), purpose(TEXT), created_at(TEXT)
3. admin_sessions … ログイン後のセッション。列: id(TEXT, PK), created_at(TEXT), expires_at(TEXT)

マイグレーションファイルは wrangler.jsoncmigrations_dir で指定したフォルダに置く。GitHub Actions 方式なら push 時に自動適用される。Git連携方式なら push 前に npx wrangler d1 migrations apply my-bbs-db --remote を手元から実行する。

4. 【バックエンド】API とミドルウェアの追加

Section titled “4. 【バックエンド】API とミドルウェアの追加”

作るのは3グループ。(A) 管理API(TKN/ADM と同じ)、(B) パスキーの登録・ログインAPI(C) 管理APIを守るミドルウェア

管理操作のAPIは /api/admin/ 配下に置く。後で /api/admin/ 配下をまとめてアクセス制限するため、公開側API(/api/posts)と区別しておくのがポイント。

Claude
以下の3点をfunctions/api/配下に追加・修正して。
1. GET /api/admin/posts を追加する。hiddenの値にかかわらず全投稿を返す
2. PATCH /api/admin/posts/:id を追加する。リクエストボディのhiddenフィールド(0か1)でpostsテーブルのhiddenカラムを更新する
3. GET /api/posts でhidden=0の投稿だけを返すようにする

公開側の /api/posts からは非表示投稿が消え、管理用の /api/admin/posts では全投稿(非表示も含む)が見える、という分担になる。

4-2. パスキーの登録・ログインAPI

Section titled “4-2. パスキーの登録・ログインAPI”

パスキーのAPIは /api/auth/ 配下に置く(ログイン前に呼ぶので、管理APIの検問の外に置く必要がある)。エンドポイントは4つ。

sequenceDiagram
    participant B as ブラウザ
    participant CF as Pages Functions
    participant D as D1
    Note over B,D: 【登録】最初の1回だけ
    B->>CF: POST /api/auth/register/options
    CF->>D: チャレンジを保存
    CF-->>B: 登録オプション(チャレンジ)
    Note over B: 端末がパスキー作成<br>(指紋・顔・PIN)
    B->>CF: POST /api/auth/register/verify(署名)
    CF->>D: チャレンジ照合 → 公開鍵を保存
    CF-->>B: 登録完了
    Note over B,D: 【ログイン】毎回
    B->>CF: POST /api/auth/login/options
    CF->>D: チャレンジを保存
    CF-->>B: 認証オプション(チャレンジ)
    Note over B: 端末が署名(指紋・顔・PIN)
    B->>CF: POST /api/auth/login/verify(署名)
    CF->>D: 公開鍵で署名検証 → セッション発行
    CF-->>B: Set-Cookie(入館証)

Claude Code に依頼する。

Claude
@simplewebauthn/server を使って、functions/api/auth/配下にパスキー認証のエンドポイントを4つ作って。RP ID と origin はリクエストのホスト名から決める(本番ドメイン固定でよい)。チャレンジは admin_challenges に保存し、検証後に削除する。
1. POST /api/auth/register/options
- generateRegistrationOptions でオプション生成。チャレンジを admin_challenges に保存
- ただし admin_credentials が1件以上あるときは、有効な admin_session が無ければ 403(最初の1個だけ登録を開放するため)
2. POST /api/auth/register/verify
- verifyRegistrationResponse で検証。成功したら credential_id・公開鍵・counter を admin_credentials に保存
3. POST /api/auth/login/options
- generateAuthenticationOptions でオプション生成。チャレンジを admin_challenges に保存
4. POST /api/auth/login/verify
- verifyAuthenticationResponse で検証(admin_credentials の公開鍵を使う)。成功したら admin_sessions に行を作り、HttpOnly・Secure・SameSite=Strict のセッション Cookie を Set-Cookie する。counter も更新する
あわせて POST /api/auth/logout(セッション削除+Cookie破棄)も作って。

誰が管理者になるか=「早い者勝ち」方式: 管理画面はログインしないと入れないが、その最初のパスキーを登録する人もまだログインしていない——という鶏と卵の問題がある。このサンプルでは一番シンプルに、admin_credentials が空のときだけ登録を開放し、最初に登録した1台(=最初のデバイス)の持ち主が管理者になる方式にした。1個登録された時点で登録口は閉じ、以降の登録はログイン必須になる(他の人は管理者になれない)。

裏を返すと、デプロイ後、自分より先に /admin を開いて登録した人がいれば、その人が管理者になってしまう(=早い者勝ち)。学習用サンプルなのでこの手軽さを優先しているが、実運用ではこの「窓」をなくしたい。その場合は登録自体を秘密の鍵で守る(次で解説)。いずれにせよ、デプロイしたらすぐ自分のパスキーを登録して窓を閉じるのが基本。

(任意)登録口をトークンで守り、早い者勝ちをなくす

Section titled “(任意)登録口をトークンで守り、早い者勝ちをなくす”

「早い者勝ち」が気になるなら、登録口にトークン認証を1枚かぶせる。これは TKN のトークン認証とまったく同じ仕組みで、SimpleWebAuthn の機能ではなく自前で足すコード(ライブラリは鍵と署名だけを担当し、誰に登録を許すかはアプリ側の責任)。x-admin-tokenx-registration-secret に読み替えただけ、と考えてよい。

「だったらトークン認証だけでよくない?」への答え: 役割が違う。この秘密の鍵が守るのは登録(最初のセットアップ1回)だけで、済んだら削除してよい。日々のログインはあくまでパスキー——毎回流れる合言葉ではなく、漏れない・フィッシングに強い・持ち主だけが使える強さはそのまま。トークン認証は「ログインの合言葉そのものが恒久的な共有秘密」だが、こちらは「入り口を一度作るためだけに使って捨てる鍵」。登録後に秘密を消せば常設の共有秘密がゼロになる。ここが決定的に違う。

  1. 登録APIに秘密の照合を足す。Claude Code に依頼する。
Claude
パスキーの登録API(/api/auth/register/options と /api/auth/register/verify)に、登録を秘密の鍵で守る処理を足して。
- リクエストヘッダ x-registration-secret が環境変数 REGISTRATION_SECRET と一致する、またはすでにログイン済み(有効な admin_session がある)ときだけ登録を許可する
- どちらでもなければ 403 を返す
- 「admin_credentials が空なら登録を開放する」という条件は削除する(早い者勝ちをやめる)
あわせて public/admin/index.html の登録フォームに秘密の鍵の入力欄を足して、登録リクエストの x-registration-secret ヘッダにのせて送るようにして。
  1. 秘密の鍵を環境変数に設定するCloudflare ダッシュボードWorkers & Pages → プロジェクト名 → SettingsVariables and Secrets で、Type を Secret にして追加する。値は推測不能なランダム文字列にする(node -e "console.log(crypto.randomUUID())" で生成できる)。
Variable nameValue
REGISTRATION_SECRET生成したランダム文字列

設定後、Retry deployment(または push)で反映する。

  1. 自分のパスキーを登録する/admin の登録欄にこの秘密の鍵を入れて登録する。秘密を知らない人は最初の1個すら登録できないので、早い者勝ちの窓が消える。

  2. 登録後の扱い。登録口を完全に閉じたいなら REGISTRATION_SECRET を削除する(以降はログイン済みのときだけ端末を追加できる)。スマホ・PC など複数端末を登録する予定なら、しばらく残しておいてもよい。

/api/admin/ 配下の全リクエストを、セッション Cookie で検問する。

sequenceDiagram
    participant B as ブラウザ
    participant MW as _middleware.js
    participant API as /api/admin/*
    B->>MW: fetch /api/admin/…(セッションCookie)
    Note over MW: Cookie のセッションIDを<br>admin_sessions と照合
    alt 有効なセッション
        MW->>API: 通す
        API-->>B: 処理結果
    else 無効/期限切れ/無し
        MW-->>B: 401
    end
Claude
functions/api/admin/_middleware.js を作って。リクエストの Cookie にあるセッションIDを admin_sessions と照合し、有効(期限内)なら通す。無効・期限切れ・無しなら 401 を返す。

これで「ログイン(パスキーで署名)→ セッション Cookie → 管理API が通る」という流れができる。

5. 【フロントエンド】管理画面の作成

Section titled “5. 【フロントエンド】管理画面の作成”

管理画面(public/admin/index.html)に、パスキーの登録/ログインUI投稿一覧を載せる。ブラウザ側のパスキー処理は @simplewebauthn/browser を ESM CDN から読み込んで使う。

Claude Code に依頼する。

Claude
管理画面 public/admin/index.html を作って。@simplewebauthn/browser は ESM CDN(https://esm.sh/@simplewebauthn/browser@13)から import する。
状態は3つ。
1. 未ログインで、まだパスキーが1つも無いとき(初回)
- 「パスキーを登録」ボタンを表示
- 押すと POST /api/auth/register/options → startRegistration() → POST /api/auth/register/verify。成功したら続けてログインする
2. 未ログインで、パスキーが登録済みのとき
- 「パスキーでログイン」ボタンを表示
- 押すと POST /api/auth/login/options → startAuthentication() → POST /api/auth/login/verify。成功したら管理画面を表示
3. ログイン済み(セッション有効)のとき
- GET /api/admin/posts で全投稿を一覧表示。テーブルで、各行に表示/非表示の切り替えボタン。非表示中の投稿はグレーアウト
- ログアウトボタン(POST /api/auth/logout)を置く
管理APIへの fetch は credentials: "include" を付けてセッションCookieを送ること。

「初回はまだパスキーが無い」かどうかは、/api/auth/login/options が返す allowCredentials が空かで判定できる(空なら未登録 =「登録」ボタン、1件以上あれば「ログイン」ボタンを表示)。ページ読み込み時にこれを1回呼んで画面を出し分ける、という素直な作りでよい。

これで管理画面はできるが、まだ本番にパスキーを登録していない。次の章でデプロイして、最初のパスキーを登録する。

6. デプロイして最初のパスキーを登録

Section titled “6. デプロイして最初のパスキーを登録”

push する前に、初回だけ Cloudflare のビルド設定を1つ変える。Pages Functions が @simplewebauthn/server を使うので、ビルド時に依存をインストールさせる必要がある。Cloudflare ダッシュボード → Workers & Pages → プロジェクト → SettingsBuild で、Build command を npm install にする(Build output directory は public のまま)。これをしないと Functions のバンドルに失敗してデプロイが落ちる

あわせて、wrangler.jsoncnodejs_compat(2章①)が入っていることを確認する。無いと登録/ログインの実行時に 500 になる。

準備ができたら、変更をコミット・push する。

Claude
今回の変更をコミットしてpushして

GitHub の Actions タブ、または Cloudflare ダッシュボードの Deployments で進行状況を確認する。Git連携方式の場合は、push 前に 3-3 のマイグレーション適用(npx wrangler d1 migrations apply my-bbs-db --remote)を済ませておく。

デプロイが Failure になったら: 多くは Build command が空欄で npm install が走っていないのが原因。上の設定をしてから Retry deployment。「/api/posts(依存なし)は動くのに /api/auth/* だけ 405/動かない」という症状も、Functions のバンドル失敗(=依存未インストール)のサイン。

デプロイが終わったら、本番 URL の /admin を開く。まだパスキーが無いので「パスキーを登録」ボタンが出る。押すと、端末の生体認証(指紋・顔・PIN)が起動し、パスキーが作られる。

パスキー登録画面

本番で最初に表示される登録画面(まだ誰も登録していない状態)

/admin で Cloudflare Access のログイン画面(Google/メール)が出たら: 同じ URL で先に ADM(Cloudflare Access) をやっていると、その Access アプリが Pages の手前(エッジ)に残っていて、パスキー画面に届く前に横取りする。Cloudflare ダッシュボード → Zero TrustAccess controlsApplications で、その /admin を保護しているアプリ(ADM で作った my-bbs-admin など)を削除してから開き直す。今回はパスキーで認証するので、Access のゲートは外してよい。

パスキーはドメインに紐づく。ここで登録したパスキーは、登録したときのドメイン(例:my-bbs-xxx.pages.dev)でしか使えない。後で独自ドメインに変えると、そのパスキーは無効になり登録し直しになる。本番で使うドメインを決めてから登録するのが安全。HTTPS は Cloudflare Pages が自動で用意するので、その点は問題ない。

最初の1個を登録すると、以降の登録口は閉じられるadmin_credentials が空でなくなり、ログインなしの登録は 403 になる)。

  • /admin を開くと、今度は「パスキーでログイン」ボタンが出る。押して生体認証を通すと管理画面に入れる
  • 表示/非表示の切り替えを試し、公開側のページから非表示投稿が消えることを確認する
  • ログアウトボタンでログイン画面に戻る
  • シークレットウィンドウで https://掲示板のURL/api/admin/posts を直接開くと 401 が返る(セッションCookieが無いため守られている)
  • 別の端末/別ブラウザからログインできるかは、パスキーが同期されるかどうかで変わる(下記)

パスキーは「端末に紐づく」とは限らない: パスキーには2種類ある。同期パスキー(iCloudキーチェーン、1Password など)は、同じ同期グループの端末——たとえば同じ Apple ID の iPhone と MacBook——で自動的に共有される。この場合、片方で1回登録すればもう片方でもそのままログインできる(追加登録は不要)。iPhone と Mac を使っている人は、これで登録1回で済むことが多い。一方 デバイス固定パスキー(ハードウェアキーや、同期しない設定)はその端末でしか使えないので、端末ごとに登録が要る。「端末に紐づく」か「同期グループ(≒その人のアカウント)に紐づく」かは、使っているパスキーの種類しだい。

別エコシステムをまたぐ(例:Apple と Windows)、あるいは同期を使っていない場合は、ログイン後にもう1つパスキーを追加登録しておくと安全(admin_credentials が空でなくても、ログイン済みなら登録を許可する 4-2 の分岐)。

TKN の2方式・ADM の Cloudflare Access に、今回のパスキーを加えた比較。

Basic認証トークン認証Cloudflare Accessパスキー(自前)
実装・設定の手間最小少ない多い(初回のみ)多い
ログイン画面ブラウザ固定自前Google自前
ログアウト事実上できないできるできるできる
守る合言葉あり(共有)あり(共有)Google に委ねるなし(生体/PIN)
フィッシング耐性弱い弱いIdP 依存強い(ドメイン束縛)
漏洩時の影響合言葉で全員入れる同左Google 次第公開鍵だけなので入れない
個人の識別できないできないメール単位パスキー単位
守れる範囲ページもAPIAPIページもAPIAPI(セッション)
外部サービス不要不要Google Cloud Console不要

パスキーは手間こそ一番かかるが、「漏れる合言葉が無い・フィッシングに強い・パスワードレス」という、ほかの方式に無い強さがある。「自分だけの管理画面を、外部サービスにも合言葉にも頼らず、いまどきの認証で守る」という用途にはまる。一方、とにかく手軽に済ませたいなら TKN のトークン認証、複数人をメール単位で管理したいなら ADM の Cloudflare Access が向く。

  • パスキーはドメインに束縛される(RP ID=ドメイン)。pages.dev で登録 → 独自ドメインに移行、のように途中でドメインが変わると登録済みパスキーは無効になる。本番ドメインを決めてから登録する
  • 秘密鍵は端末から出ず、サーバーが持つのは公開鍵だけ。D1 が漏れてもログインはできない(合言葉方式に対する明確な利点)
  • チャレンジは使い捨て。発行時に保存し、検証後に必ず削除する(使い回しを防ぐ)。古いチャレンジは期限切れで掃除する設計にしておくとよい
  • セッションCookie は HttpOnly・Secure・SameSite=Strict で発行する。JS から読めず、HTTPS のみ、別サイトからの送信を防ぐ
  • 端末紛失や機種変に備え、複数のパスキーを登録できるようにしておくと安全。ただし iCloudキーチェーンなどの同期パスキーなら、同じ Apple ID の端末(iPhone と Mac など)は1回の登録で共有されるので、追加登録が要るのは主に別エコシステム(Windows など)や同期しない設定のとき。「最初の1個だけ登録開放、以降はログイン必須」の設計なら、ログイン後に追加登録できる
  • 管理者の登録は「早い者勝ち」(最初に登録した1台が管理者、以降は登録ロック)。デプロイしたらすぐ自分のパスキーを登録して窓を閉じる。先に他人に登録される窓をそもそも無くしたいなら、登録口を秘密の鍵(環境変数)で守る方式にする(§4-2)
  • @simplewebauthn/server は Web Crypto ベースで Pages Functions(Workers ランタイム)で動く。ビルドで Node API を要求された場合は wrangler.jsoncnodejs_compat フラグを足す