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) Google
Safari (macOS/iOS) iCloud
Firefox なし
Edge Google
Android Chrome Google
iOS Safari iCloud

まとめ

Passkeyはパスワードの終焉をもたらします—公開鍵暗号に基づき、フィッシング耐性、漏洩耐性、リプレイ耐性を備えています。2026年までにすべての主要ブラウザとOSがサポートしており、本番環境へのデプロイの時が来ています。@simplewebauthnライブラリで迅速に統合し、条件付きUIと組み合わせてユーザーにシームレスな切り替えを提供します。フォールバック戦略を忘れずに:Passkey優先、パスワードはバックアップ、段階的強化。

ブラウザローカルツールを無料で試す →

#WebAuthn#Passkey#无密码认证#FIDO2#生物识别