一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • パスキー認証の実装:パスワードレスな未来への移行手順

パスワード認証の限界とパスキーの登場

パスワード認証の問題点は長年にわたり議論されてきました。フィッシング攻撃、パスワードリスト攻撃、ユーザーによる使い回し、そして運用コストの肥大化。FIDO2/WebAuthnの標準化とApple・Google・Microsoftによるパスキーのサポートにより、パスワードレス認証が現実的な選択肢となりました。

パスキーは公開鍵暗号方式を基盤としており、秘密鍵はユーザーのデバイスに安全に保管されます。サーバー側には公開鍵しか保存されないため、サーバー侵害時の影響が最小限に抑えられます。さらに、パスキーはフィッシング耐性を持っており、正規のドメインでのみ認証が成立する仕組みだ。本記事では、Webアプリケーションにパスキー認証を実装する具体的な手順を解説します。

WebAuthn APIの基本的な仕組み

登録フロー(Registration)

パスキーの登録は、サーバーからチャレンジを取得し、ブラウザのWebAuthn APIを通じて新しい鍵ペアを生成するプロセスだ。ユーザーは生体認証やPINで本人確認を行い、生成された公開鍵がサーバーに送信されます。

// サーバー側:登録オプションの生成(Node.js + @simplewebauthn/server)
import {
generateRegistrationOptions,
verifyRegistrationResponse
} from '@simplewebauthn/server';

const rpName = 'My Application';
const rpID = 'example.com';
const origin = 'https://example.com';

async function getRegistrationOptions(user) {
const options = await generateRegistrationOptions({
rpName,
rpID,
userID: user.id,
userName: user.email,
attestationType: 'none',
authenticatorSelection: {
residentKey: 'preferred',
userVerification: 'preferred',
},
supportedAlgorithmIDs: [-7, -257],
});

// チャレンジをセッションに保存
await saveChallenge(user.id, options.challenge);
return options;
}

クライアント側の実装

ブラウザ側ではnavigator.credentials.create()を使って認証器と対話します。SimpleWebAuthnのクライアントライブラリを使用すると、ブラウザ互換性の差異を吸収できます。

// クライアント側:パスキーの登録
import { startRegistration } from '@simplewebauthn/browser';

async function registerPasskey() {
try {
const optionsResponse = await fetch('/api/auth/register/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const options = await optionsResponse.json();

const registration = await startRegistration(options);

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

if (result.verified) {
alert('パスキーの登録が完了しました');
}
} catch (error) {
if (error.name === 'NotAllowedError') {
console.log('ユーザーが認証をキャンセルしました');
}
}
}

認証フロー(Authentication)の実装

// サーバー側:認証オプションの生成
async function getAuthenticationOptions(userEmail) {
const user = await findUserByEmail(userEmail);
const userPasskeys = await getUserPasskeys(user.id);

const options = await generateAuthenticationOptions({
rpID,
allowCredentials: userPasskeys.map(passkey => ({
id: passkey.credentialID,
type: 'public-key',
transports: passkey.transports,
})),
userVerification: 'preferred',
});

await saveChallenge(user.id, options.challenge);
return options;
}

// サーバー側:認証レスポンスの検証
async function verifyAuth(userId, response) {
const expectedChallenge = await getChallenge(userId);
const passkey = await getPasskeyById(response.id);

const verification = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin: origin,
expectedRPID: rpID,
authenticator: {
credentialPublicKey: passkey.publicKey,
credentialID: passkey.credentialID,
counter: passkey.counter,
},
});

if (verification.verified) {
await updatePasskeyCounter(passkey.id, verification.authenticationInfo.newCounter);
}
return verification.verified;
}

移行戦略:段階的なパスワードレス化

既存のパスワード認証からパスキーへの移行は段階的に行うべきだ。筆者が推奨する移行ステップは以下の通りです。

  1. 第1段階:パスワード+パスキーの併用期間を設ける。ユーザーはどちらの方法でもログインできます
  2. 第2段階:パスキー登録済みユーザーにはパスキーを優先表示し、パスワード入力をオプションにする
  3. 第3段階:新規登録ユーザーはパスキーのみとし、既存ユーザーにはパスキー登録を促す
  4. 第4段階:一定期間後にパスワード認証を廃止する(十分な移行率を確認後)

フォールバック戦略

パスキーに対応していないブラウザや環境も考慮する必要があります。WebAuthnの対応状況を確認し、非対応の場合は従来の認証方式にフォールバックする設計が重要です。

// パスキー対応チェック
async function isPasskeySupported() {
if (!window.PublicKeyCredential) return false;
try {
const available = await PublicKeyCredential
.isUserVerifyingPlatformAuthenticatorAvailable();
return available;
} catch {
return false;
}
}

まとめ

パスキー認証は、セキュリティとユーザー体験の両方を向上させる技術だ。実装にはWebAuthn APIの理解が必要だが、SimpleWebAuthnのようなライブラリを活用すれば開発工数を大幅に削減できます。まずは既存のパスワード認証と併用する形で導入を始め、ユーザーの反応を見ながら段階的にパスワードレスへ移行していくことを推奨します。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア