WebAuthnとPasskey:パスワード時代に別れを告げるパスワードレス認証の完全実践
前端工程(更新: 2026年6月2日)
パスワードは死んだ、Passkey万歳
パスワードの根本的問題:パスワードは共有秘密です—サーバーとユーザーの両方が知っており、どちらかが漏洩すれば安全ではありません。
| 問題 | 影響 |
|---|---|
| 弱いパスワード | データ漏洩の81%は弱いパスワードが原因 |
| パスワード再利用 | 1つの漏洩で全アカウントが危険に |
| フィッシング攻撃 | ユーザーは本物と偽サイトを区別できない |
| データベース漏洩 | サーバーに保存されたハッシュが解読される可能性 |
| MFA SMSハイジャック | SIMスワップ攻撃 |
**Passkey(WebAuthn/FIDO2)**は公開鍵暗号でパスワードを置き換えます:
パスワードモデル:ユーザー → [パスワード] → サーバー(パスワードハッシュを保存)
Passkeyモデル: ユーザー → [署名] → サーバー(公開鍵のみ保存)
公開鍵から秘密鍵を導出することはできません。サーバーが侵害されても、攻撃者はユーザーになりすませません。
WebAuthnプロトコルフロー
登録フロー
1. ユーザーが「Passkeyを登録」をクリック
2. サーバーがチャレンジを生成 → フロントエンドに送信
3. ブラウザが認証器を呼び出し → ユーザー検証(指紋/Face ID/PIN)
4. 認証器が鍵ペアを生成 → 公開鍵 + 署名を返す
5. フロントエンドが公開鍵 + 署名をサーバーに送信
6. サーバーが署名を検証 → 公開鍵(クレデンシャル)を保存
認証フロー
1. ユーザーが「Passkeyでログイン」をクリック
2. サーバーがチャレンジを生成 → フロントエンドに送信
3. ブラウザが認証器を呼び出し → ユーザー検証
4. 認証器が秘密鍵でチャレンジに署名 → 署名を返す
5. フロントエンドが署名をサーバーに送信
6. サーバーが公開鍵で署名を検証 → 認証成功
完全なコード実装
サーバー:登録開始
// POST /api/webauthn/register/start
import { generateRegistrationOptions } from '@simplewebauthn/server';
export async function registerStart(userId: string, username: string) {
const options = await generateRegistrationOptions({
rpID: 'example.com',
rpName: 'ToolsKu',
userID: userId,
userName: username,
attestationType: 'none',
authenticatorSelection: {
authenticatorAttachment: 'platform', // プラットフォーム認証器を優先
residentKey: 'required', // ディスカバラブルクレデンシャル(Passkey)
userVerification: 'preferred',
},
});
// 後続検証用にチャレンジを保存
await storeChallenge(userId, options.challenge);
return options;
}
フロントエンド:登録
import { startRegistration } from '@simplewebauthn/browser';
async function registerPasskey() {
try {
// 1. サーバーから登録オプションを取得
const options = await fetch('/api/webauthn/register/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, username: user.name }),
}).then(r => r.json());
// 2. ブラウザAPIを呼び出して登録
const credential = await startRegistration({ optionsJSON: options });
// 3. クレデンシャルをサーバーに送信して検証
const result = await fetch('/api/webauthn/register/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, credential }),
}).then(r => r.json());
if (result.verified) {
showToast('Passkeyの登録が成功しました!');
}
} catch (error) {
if (error.name === 'NotAllowedError') {
showToast('ユーザーが操作をキャンセルしました');
}
}
}
サーバー:登録完了
import { verifyRegistrationResponse } from '@simplewebauthn/server';
export async function registerFinish(userId: string, credential: any) {
const challenge = await getChallenge(userId);
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: challenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
});
if (verification.verified) {
// クレデンシャル(公開鍵)を保存
await storeCredential(userId, {
id: verification.registrationInfo?.credentialID,
publicKey: verification.registrationInfo?.credentialPublicKey,
counter: verification.registrationInfo?.counter,
deviceType: verification.registrationInfo?.credentialDeviceType,
});
}
return { verified: verification.verified };
}
フロントエンド:認証
import { startAuthentication } from '@simplewebauthn/browser';
async function authenticateWithPasskey() {
try {
// 1. 認証オプションを取得
const options = await fetch('/api/webauthn/auth/start', {
method: 'POST',
}).then(r => r.json());
// 2. ブラウザAPIを呼び出して認証
const credential = await startAuthentication({ optionsJSON: options });
// 3. 署名を検証
const result = await fetch('/api/webauthn/auth/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential }),
}).then(r => r.json());
if (result.verified) {
redirect('/dashboard');
}
} catch (error) {
if (error.name === 'NotAllowedError') {
showToast('認証がキャンセルされました');
}
}
}
プラットフォーム認証器 vs ローミング認証器
| 次元 | プラットフォーム認証器 | ローミング認証器 |
|---|---|---|
| 例 | Touch ID, Face ID, Windows Hello | YubiKey, Titan Key |
| 保存場所 | デバイス内TPM/Secure Enclave | ハードウェアセキュリティチップ |
| クロスデバイス | iCloud/Google同期 | 物理的な携帯が必要 |
| コスト | 無料 | $25-70 |
| ユーザー体験 | 指紋/顔、シームレス | 挿入/タッチ |
| 紛失リスク | デバイス紛失 | キー紛失 |
Passkey同期メカニズム
Apple iCloudキーチェーン:
iPhoneでPasskey登録 → iCloud同期 → Mac/iPadで自動利用可能
Googleパスワードマネージャー:
AndroidでPasskey登録 → Google同期 → Chrome全プラットフォームで利用可能
Windows Hello:
ローカルTPM保存 → クロスデバイス同期なし
→ プラットフォーム + ローミングPasskeyの両方登録を推奨
フォールバック戦略:段階的デプロイ
function AuthForm() {
const [supportsPasskey, setSupportsPasskey] = useState(false);
useEffect(() => {
// WebAuthnサポートを検出
setSupportsPasskey(
window.PublicKeyCredential !== undefined &&
typeof window.PublicKeyCredential === 'function'
);
}, []);
return (
<form>
{supportsPasskey && (
<Button onClick={authenticateWithPasskey}>
Passkeyでログイン
</Button>
)}
{/* 従来のパスワードをフォールバックとして */}
<Input type="email" placeholder="メールアドレス" />
<Input type="password" placeholder="パスワード" />
<Button type="submit">ログイン</Button>
</form>
);
}
条件付きUI:Passkey自動検出
// ユーザーが登録済みPasskeyを持っているか確認
const isConditionalAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
if (isConditionalAvailable) {
// ブラウザがパスワード欄にPasskeyオプションを自動表示
const credential = await startAuthentication({
optionsJSON: options,
useAutocomplete: true, // 条件付きUIを有効化
});
}
<!-- パスワード欄にPasskeyオプションが自動表示される -->
<input
type="password"
autocomplete="webauthn"
placeholder="パスワードまたはPasskey"
/>
セキュリティ考慮事項
1. チャレンジのセキュリティ
// ✅ チャレンジはサーバー生成の乱数でなければならない
const challenge = crypto.randomUUID();
// ❌ 固定値やタイムスタンプを絶対に使用しない
const badChallenge = Date.now().toString();
2. Origin検証
// ✅ Originを厳密に検証し、フィッシングを防止
const expectedOrigin = 'https://example.com';
// ❌ ワイルドカードを使用しない
const badOrigin = '*';
3. クレデンシャルカウンター
// クローンクレデンシャルを検出
if (newCounter <= storedCounter) {
// クレデンシャルがクローンされた可能性あり!
throw new Error('クローンされた認証器の可能性があります');
}
4. マルチクレデンシャル戦略
推奨:ユーザーごとに少なくとも2つのPasskeyを登録
- 1つのプラットフォームPasskey(日常使用)
- 1つのローミングPasskeyまたはバックアップPasskey(復旧用)
ブラウザとプラットフォームのサポート
| プラットフォーム | Passkeyサポート | 同期 | 条件付きUI |
|---|---|---|---|
| Chrome (Windows/macOS) | ✅ | ✅ | |
| Safari (macOS/iOS) | ✅ | iCloud | ✅ |
| Firefox | ✅ | なし | ❌ |
| Edge | ✅ | ✅ | |
| Android Chrome | ✅ | ✅ | |
| iOS Safari | ✅ | iCloud | ✅ |
まとめ
Passkeyはパスワードの終焉をもたらします—公開鍵暗号に基づき、フィッシング耐性、漏洩耐性、リプレイ耐性を備えています。2026年までにすべての主要ブラウザとOSがサポートしており、本番環境へのデプロイの時が来ています。@simplewebauthnライブラリで迅速に統合し、条件付きUIと組み合わせてユーザーにシームレスな切り替えを提供します。フォールバック戦略を忘れずに:Passkey優先、パスワードはバックアップ、段階的強化。
ブラウザローカルツールを無料で試す →
#WebAuthn#Passkey#无密码认证#FIDO2#生物识别