🗝️

Next.jsでPasskey認証のデモを実装した

に公開

はじめに

以下の記事を読んで Passkey を実装したくなったため、デモを作ってみました。
https://blog.jxck.io/entries/2025-07-07/load-to-passkey-0.html

Passkeyとは何か

パスワードを使わずにログインできる仕組みです。
公開鍵と秘密鍵を使って認証を行っていて、秘密鍵の部分をクラウドに保存することで様々なデバイスで共有することができます。
秘密鍵の保存を担当するのは以下のようなサービスです:

  • iCloud Keychain
  • Google Password Manager
  • 1Password

一方、ウェブサービス提供者(アプリ開発者)は公開鍵のみを保存します。
これにより、パスワード漏洩のリスクを根本的に排除できます。

ブラウザからは、WebAuthnと呼ばれるAPIを使用してパスキーを利用することができます。
日本語で説明された記事がいくつかあります。

実装

認証サーバーおよびフロントエンドの実装は、SimpleWebAuthn を使用します。
SimpleWebAuthn を使用した実装例については SimpleWebAuthn のリポジトリから確認することができます。

https://github.com/MasterKale/SimpleWebAuthn/tree/master/example

作ったものはこれです。Next.js を使って、フロントエンドと認証サーバーの両方を実装しました。
https://github.com/rie03p/passkey-nextjs

認証サーバー

Passkeyの認証をするにあたってサーバー側では4つのエンドポイントを用意する必要があります。
基本的には SimpleWebAuthn を使用するだけで完結していて、自分で実装しないといけないのは生成されたオプションのうちcurrentChallengeを保存して検証の時に参照できるようにするだけです。
詳細はリポジトリのsrc/app/apiを確認してください。

  • /registration/options: 登録オプション生成
  • /registration/verify: パラメータ検証、登録
  • /authentication/option: 認証オプション生成
  • /authentication/verify: パラメータ検証

フロントエンド

認証サーバーで実装した API を呼び出していきます。

Passkeyの登録

SimpleWebAuthn のstartRegistrationを呼び出すことで、Passkey の登録が要求されます。

// Passkeyの登録処理
const register = useCallback(async () => {
    try {
      // 1. サーバーから登録用のチャレンジ情報を取得
      const optionsResponse = await fetch('/api/registration/options', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
      });

      if (!optionsResponse.ok) {
        throw new Error('Failed to fetch registration options');
      }

      // 2. ブラウザのWebAuthn APIで認証器を使って署名を作成
      const options = (await optionsResponse.json()) as PublicKeyCredentialCreationOptionsJSON;
      const attestationResponse = await startRegistration({optionsJSON: options});

      // 3. サーバーで署名を検証してPasskeyを保存
      const verificationResponse = await fetch('/api/registration/verify', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(attestationResponse satisfies RegistrationResponseJSON),
      });

      if (!verificationResponse.ok) {
        throw new Error('Failed to verify registration response');
      }

      // 4. 検証結果を確認
      const verification = (await verificationResponse.json()) as registrationVerifyResponse;

      if (verification.verified) {
        const username = verification.user?.username ?? 'demo user';
        setMessage(`Authenticated as ${username}`);
      } else {
        setMessage('Authentication failed, please try again');
      }
    } catch (error) {
      setMessage(error instanceof Error ? error.message : 'Registration failed unexpectedly');
    } finally {
      setStatus('idle');
    }
  }, []);

認証

SimpleWebAuthn のstartAuthenticationを呼び出すことで、Passkey での認証を要求されます。

 // Passkeyでの認証処理
  const authenticate = useCallback(async () => {
    try {
      // 1. サーバーから認証用のチャレンジ情報を取得
      const optionsResponse = await fetch('/api/authentication/options', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
      });

      if (!optionsResponse.ok) {
        throw new Error('Failed to fetch authentication options');
      }

      const options = (await optionsResponse.json()) as PublicKeyCredentialRequestOptionsJSON;

      // 2. ブラウザのWebAuthn APIで保存済みPasskeyを使って署名
      const assertionResponse = await startAuthentication({optionsJSON: options});

      // 3. サーバーで署名を検証してユーザーを認証
      const verificationResponse = await fetch('/api/authentication/verify', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(assertionResponse satisfies AuthenticationResponseJSON),
      });

      if (!verificationResponse.ok) {
        throw new Error('Failed to verify authentication response');
      }

      // 4. 検証結果を確認
      const verification = (await verificationResponse.json()) as registrationVerifyResponse;

      if (verification.verified) {
        const username = verification.user?.username ?? 'demo user';
        setMessage(`Authenticated as ${username}`);
      } else {
        setMessage('Authentication failed, please try again');
      }
    } catch (error) {
      setMessage(error instanceof Error ? error.message : 'Authentication failed unexpectedly');
    } finally {
      setStatus('idle');
    }
  }, []);

動作確認

以下の手順で動作確認をします。

> git clone https://github.com/rie03p/passkey-nextjs.git
> cd passkey-nextjs
> pnpm i
> pnpm dev

http://localhost:3000にアクセスし、Register Passkeyをクリックすることで、Passkeyの登録が促されると思います。(私の場合は1password)

Passkeyを保存し、 Sign in with Passkeyをクリックすることで、Passkeyを使用したログインができます。

まとめ

Next.js を使用し、SimpleWebASuth を使用した Passkey デモを作ってみましたが、SimpleWebAuthn が仕様周りをいい感じに吸収してくれていて簡単に実装できて驚きました。

Discussion