mcpField
W
webauthn

webauthn

github.com/claude-dev-suiteセキュリティ4661 回インストール2 日前に更新
リポジトリ

webauthn について

Web アプリに認証機能を実装する開発者向けの、WebAuthn とパスキーの実践ガイドです。SimpleWebAuthn による登録・ログイン、ブラウザーからの呼び出し、本番環境での注意点を扱います。

主な機能

  1. 01登録オプションの生成と検証
  2. 02認証オプションの生成と検証
  3. 03ブラウザーでの登録・ログイン例
  4. 04チャレンジとカウンターの保護策
  5. 05認証情報管理のチェックリスト

活用シーン

  1. 01Web アプリへのパスキー登録追加
  2. 02サーバー検証を伴うパスキーログインの実装
  3. 03WebAuthn フローのリプレイリスク確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill webauthn

インストール後、Claude Code で/webauthnを入力して読み込まれたか確認します。

詳細

WebAuthn / パスキー

サーバーのセットアップ(SimpleWebAuthn)

import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';

const rpName = 'My App';
const rpID = 'myapp.com';
const origin = 'https://myapp.com';

登録フロー

サーバー:オプションの生成
app.post('/api/auth/register/options', auth, async (req, res) => {
  const user = req.user;
  const existingCredentials = await db.credential.findMany({
    where: { userId: user.id },
  });

  const options = await generateRegistrationOptions({
    rpName,
    rpID,
    userName: user.email,
    userDisplayName: user.name,
    excludeCredentials: existingCredentials.map((c) => ({
      id: c.credentialId,
      transports: c.transports,
    })),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'preferred',
    },
  });

  // Store challenge for verification
  await db.challenge.upsert({
    where: { userId: user.id },
    create: { userId: user.id, challenge: options.challenge },
    update: { challenge: options.challenge },
  });

  res.json(options);
});
サーバー:登録の検証
app.post('/api/auth/register/verify', auth, async (req, res) => {
  const user = req.user;
  const { challenge } = await db.challenge.findUnique({ where: { userId: user.id } });

  const verification = await verifyRegistrationResponse({
    response: req.body,
    expectedChallenge: challenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
  });

  if (verification.verified && verification.registrationInfo) {
    const { credential } = verification.registrationInfo;
    await db.credential.create({
      data: {
        userId: user.id,
        credentialId: Buffer.from(credential.id),
        publicKey: Buffer.from(credential.publicKey),
        counter: credential.counter,
        transports: req.body.response.transports,
      },
    });
  }

  res.json({ verified: verification.verified });
});

認証フロー

サーバー:オプションの生成
app.post('/api/auth/login/options', async (req, res) => {
  const options = await generateAuthenticationOptions({
    rpID,
    userVerification: 'preferred',
    // For passkeys (discoverable credentials), omit allowCredentials
  });

  // Store challenge in session
  req.session.challenge = options.challenge;
  res.json(options);
});
サーバー:認証の検証
app.post('/api/auth/login/verify', async (req, res) => {
  const credential = await db.credential.findUnique({
    where: { credentialId: Buffer.from(req.body.id, 'base64url') },
  });

  const verification = await verifyAuthenticationResponse({
    response: req.body,
    expectedChallenge: req.session.challenge,
    expectedOrigin: origin,
    expectedRPID: rpID,
    credential: {
      id: credential.credentialId,
      publicKey: credential.publicKey,
      counter: credential.counter,
    },
  });

  if (verification.verified) {
    // Update counter (replay protection)
    await db.credential.update({
      where: { id: credential.id },
      data: { counter: verification.authenticationInfo.newCounter },
    });

    const token = generateJWT(credential.userId);
    res.json({ verified: true, token });
  }
});

ブラウザクライアント

import { startRegistration, startAuthentication } from '@simplewebauthn/browser';

// Register
async function registerPasskey() {
  const optionsRes = await fetch('/api/auth/register/options', { method: 'POST' });
  const options = await optionsRes.json();

  const credential = await startRegistration({ optionsJSON: options });

  const verifyRes = await fetch('/api/auth/register/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential),
  });
  return (await verifyRes.json()).verified;
}

// Login
async function loginWithPasskey() {
  const optionsRes = await fetch('/api/auth/login/options', { method: 'POST' });
  const options = await optionsRes.json();

  const credential = await startAuthentication({ optionsJSON: options });

  const verifyRes = await fetch('/api/auth/login/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credential),
  });
  return (await verifyRes.json()).token;
}

アンチパターン

アンチパターン修正方法
カウンターを保存しないリプレイ攻撃を防ぐため、カウンターを保存して検証する
チャレンジを再利用するセレモニーごとに新しいチャレンジを生成する
フォールバック認証方法がないフォールバックとしてパスワード/マジックリンクを提供する
rpID をハードコードする環境変数から設定し、ドメインと一致させる
exclude に transports がない再登録時の UX を向上させるため、transports を保存する

本番環境チェックリスト

  • HTTPS が必須(WebAuthn ではセキュアコンテキストが必須です)
  • セレモニーごとにチャレンジを生成し、有効期限を設定する
  • カウンターを保存して検証する
  • ユーザーごとに複数の認証情報をサポートする
  • 認証情報の管理 UI(表示、取り消し)
  • フォールバック認証方法を利用できる

コメント