目的
パスワードを使わずにログインできる仕組みを、WebAuthn (Passkey) + Firebase + Cloud Functions で実装しました。実装時に詰まったポイントを中心に、全体像をメモとして残します。
参考ドキュメント
WebAuthn/Passkeyを導入するメリット
WebAuthnは、従来のID/パスワードではなく公開鍵暗号方式を用いる認証規格です。
- フィッシング耐性: ドメイン(RP ID)がブラウザレベルで検証されるため、偽サイトに認証情報を送ることが構造上不可能です
- パスワードレス: ユーザーは生体認証(Touch ID/Face ID)やデバイスのロック解除だけでログインできます
- 秘密情報の非保持: サーバー側には「公開鍵」のみを保存し、ログインに必要な「秘密鍵」はユーザー端末の外に出ないため、サーバー流出時のリスクが極めて低いです
登録(Registration)の流れ
Passkeyの登録は大きく分けて以下の4ステップです
1. registerRequest(サーバー側)
役割: 認証のchallenge を発行し、セッションに一時保存します
-
Endpoint:
POST /passkey/registerRequest - 重要ポイント:
-
challenge: ランダムなバイト列。リプレイ攻撃防止のため、必ずサーバーで検証用に一時保存します -
rp.id: 実行ドメインと一致させる必要があります(localhostならlocalhost) -
residentKey: "required": これを指定することで、ユーザー名の入力なしでログインできる「パスキー」として保存されます
2. ブラウザ側で鍵ペア生成
ブラウザ標準の navigator.credentials.create() を呼び出します
// サーバーから取得したJSON(Base64URL)をUint8Arrayに変換して渡す
const credential = await navigator.credentials.create({
publicKey: {
...options,
challenge: base64urlToUint8Array(options.challenge),
user: {
...options.user,
id: base64urlToUint8Array(options.user.id)
}
}
});
注意: WebAuthn APIは
ArrayBufferを期待するため、JSONでやり取りする際は Base64URL ↔ Uint8Array の相互変換が必須です
3. registerResponse(サーバー側)
役割: ブラウザから届いた署名(Attestation)を検証し、公開鍵を保存します
- 検証項目:
- 保存していた
challengeと一致するか -
originが期待するもの(自社サイト)か -
rpIdが正しいか
- 完了処理: 検証成功後、Firestoreのユーザードキュメントに Credential ID と Public Key を紐付けて保存します
ログイン(Authentication)の流れ
登録時と似ていますが、検証対象が異なります
-
loginRequest: サーバーから
challengeを受け取る -
ブラウザ:
navigator.credentials.get()で署名を生成 - loginResponse: サーバー側で、保存済みの「公開鍵」を使って届いた署名を検証する
Cloud Functions (Firebase) でハマったところ
1. ステートレスな環境でのChallenge管理
Cloud Functionsはリクエストごとにセッション維持されないため、メモリ上で challenge を保持できません
解決策: Firestoreに一時セッションを作成する
// passkeySessions/{sessionId} に保存
{
challenge: "...",
uid: "...",
expiresAt: Timestamp // 2〜5分程度の短寿命に設定
}
registerResponse 時には、クライアントから送られた sessionId を元にFirestoreから challenge を引き当てて検証します。
2. ライブラリ選定
WebAuthnのバイナリ検証を自前で行うのは非常に困難です。Node.js環境であれば、以下のライブラリがデファクトスタンダードです
これらを使うと、面倒なバイナリ/Base64変換や署名検証ロジックを大幅に簡略化できます
まとめ
- WebAuthnの実装は「バイナリデータの扱い」と「サーバー側でのステート管理」が肝
- Firebase Cloud Functionsを使う場合は、Firestoreをうまく活用してステートレスな制限をカバーする必要がある