DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Design WebAuthn Passkey Authentication with Claude Code: Passwordless FIDO2

Introduction

Passwords are outdated — implement passkey authentication with WebAuthn (FIDO2). Let Claude Code design TouchID/FaceID/Windows Hello authentication.

CLAUDE.md Rules

## WebAuthn Design Rules
- Challenge: random 32 bytes per request
- rpID: fixed production domain (anti-phishing)
- userVerification: required (force biometrics)
- signCount validation: detect replay attacks
- Clone detection: invalidate if signCount regresses
Enter fullscreen mode Exit fullscreen mode

Generated Implementation

// src/auth/webauthn.ts
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';

// Registration: prevent session fixation
export async function startRegistration(userId: string) {
  const options = await generateRegistrationOptions({
    rpName: RP_NAME,
    rpID: RP_ID,
    userID: Buffer.from(userId),
    authenticatorSelection: {
      residentKey: 'preferred',
      userVerification: 'required',
    },
  });

  await redis.set(`webauthn:reg:challenge:${userId}`, options.challenge, { EX: 60 });
  return options;
}

// Authentication with clone detection
export async function finishAuthentication(sessionId: string, credential: any) {
  const passkey = await prisma.passkey.findFirst({
    where: { credentialId: credential.id },
  });

  const verification = await verifyAuthenticationResponse({
    response: credential,
    expectedChallenge,
    expectedOrigin: ORIGIN,
    expectedRPID: RP_ID,
    authenticator: {
      credentialPublicKey: Buffer.from(passkey.publicKey, 'base64'),
      counter: passkey.counter,
    },
    requireUserVerification: true,
  });

  // Clone detection: signCount regression
  if (verification.authenticationInfo.newCounter < passkey.counter) {
    await prisma.passkey.update({
      where: { id: passkey.id },
      data: { revokedAt: new Date(), revokedReason: 'counter_regression' },
    });
    throw new SecurityError('Passkey may be cloned');
  }

  await prisma.passkey.update({
    where: { id: passkey.id },
    data: { counter: verification.authenticationInfo.newCounter },
  });

  return { userId: passkey.userId };
}
Enter fullscreen mode Exit fullscreen mode

Summary

  1. Challenge: random 32 bytes stored in Redis for 60s
  2. signCount regression detection for clone protection
  3. discoverable credentials: no email input required
  4. Multiple device management with device names

Review with **Security Pack (¥1,480)* /security-check at prompt-works.jp*

myouga (@myougatheaxo) — Axolotl VTuber.

Top comments (0)