パスワードよ、さらば:Node.jsとSimpleWebAuthnでPasskey (WebAuthn) を実装する

Development tutorial - IT technology blog
Development tutorial - IT technology blog

「パスワード忘れ」という悩み

ユーザーがパスワードのリセットにどれだけの時間を費やしているか、統計をとったことはありますか?開発者にとっても、パスワード管理は容易ではありません。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. 認証と鍵の保存

ユーザーのスキャンが完了したら、サーバー側でその署名を検証する必要があります。有効であれば、今後のログインのためにPublicKeyCredentialIDデータベースに保存します。

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は投資に値するか?

Share: