「パスワード忘れ」という悩み
ユーザーがパスワードのリセットにどれだけの時間を費やしているか、統計をとったことはありますか?開発者にとっても、パスワード管理は容易ではありません。Brute-force対策やフィッシング防止、さらには安全なハッシュ化のためにBcryptとArgon2のどちらを選ぶべきかなど、常に頭を悩ませています。
実際、従来のパスワードは時代遅れになりつつあります。どれだけ複雑なパスワードを要求しても、ユーザーは使い回す傾向があり、偽サイトに騙されて入力してしまうリスクも排除できません。そこで解決策として登場したのが、WebAuthnに基づくPasskeyです。サーバーに機密情報を送信することなく、指紋やFaceID、デバイスのPINコードでログインが可能になります。
なぜPasskeyは圧倒的に安全なのか?
この仕組みは公開鍵暗号(Public Key Cryptography)に基づいています。サーバー側には公開鍵(Public Key)のみを保存します。ユーザーがログインする際、デバイス内のセキュリティチップにある秘密鍵(Private Key)を使用して認証メッセージに署名します。
FIDO Alliance의レポートによると、Passkeyを導入したシステムではログイン時間が最大50%短縮されるといいます。最近のプロジェクトで試した際も、非常に印象的な結果が得られました。万が一データベースが流出しても、顧客のスマートフォン内にある秘密鍵がなければ、ハッカーは手出しができません。
認証方式の比較
| 項目 | 従来のパスワード | 2要素認証 (OTP/SMS) | Passkey (WebAuthn) |
|---|---|---|---|
| ユーザー体験 | 低(記憶と入力が必要) | 煩わしい(SMS待ち、コード入力) | スムーズ(指紋/FaceIDに触れるだけ) |
| フィッシング耐性 | なし | 部分的 | 完璧 |
| 実装難易度 | 低 | 中 | 中 |
環境構築
素早く実装するために、@simplewebauthnライブラリの使用をお勧めします。バイナリデータとJSONの変換を非常にスマートに処理してくれます。
まずはプロジェクトの初期化から始めましょう:
mkdir node-passkey-demo
cd node-passkey-demo
npm init -y
npm install express @simplewebauthn/server @simplewebauthn/browser express-session
実装のステップ
1. Passkeyの登録 (Registration)
最初に、サーバーはクライアントに送信するためのオプション(Options)を生成する必要があります。最も重要なのはchallengeです。これはリプレイ攻撃(過去の通信を再利用する攻撃)を防ぐためのランダムな文字列です。
const { generateRegistrationOptions } = require('@simplewebauthn/server');
app.get('/generate-registration-options', (req, res) => {
const options = generateRegistrationOptions({
rpName: 'My App',
rpID: 'localhost',
userID: 'user123',
userName: '[email protected]',
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred',
},
});
req.session.currentChallenge = options.challenge;
res.json(options);
});
ブラウザ側では、以下のコードを使用してOS의認証ダイアログを呼び出します:
import { startRegistration } from '@simplewebauthn/browser';
async function register() {
const resp = await fetch('/generate-registration-options');
const options = await resp.json();
// ブラウザが指紋認証/FaceIDのスキャンを表示します
const regResp = await startRegistration(options);
await fetch('/verify-registration', {
method: 'POST',
body: JSON.stringify(regResp),
headers: { 'Content-Type': 'application/json' }
});
}
2. 認証と鍵の保存
ユーザーのスキャンが完了したら、サーバー側でその署名を検証する必要があります。有効であれば、今後のログインのためにPublicKeyとCredentialIDをデータベースに保存します。
const { verifyRegistrationResponse } = require('@simplewebauthn/server');
app.post('/verify-registration', async (req, res) => {
const verification = await verifyRegistrationResponse({
response: req.body,
expectedChallenge: req.session.currentChallenge,
expectedOrigin: 'http://localhost:3000',
expectedRPID: 'localhost',
});
if (verification.verified) {
// registrationInfo.credentialPublicKeyをDBに保存
res.status(200).send({ ok: true });
}
});
実装における実践的なアドバイス
WebAuthnを扱う中で、以下の3つの重要なポイントに気づきました:
- HTTPSが必須: WebAuthnはHTTP上では動作しません。唯一の例外は、デバッグに便利な
localhostです。 - データの自己パースは避ける: 返される鍵は扱いづらいArrayBuffer形式であることが多いです。セキュリティ上のロジックミスを防ぐため、信頼できるライブラリを使用しましょう。
- 常にバックアッププランを用意する: すぐにパスワードを廃止するのではなく、まずはユーザーが慣れるための並行オプションとしてPasskeyを導入しましょう。
Passkeyは投資に値するか?

