The authentication ceremony

Signing in works the same shape as registering: a fresh challenge from the server, a call into the real WebAuthn API, and a signature the server verifies — except this time the authenticator signs with a key it already has, over authenticatorData ‖ SHA-256(clientDataJSON), and the server checks that signature against the public key it stored during registration.

1. The browser calls navigator.credentials.get()

const credential = (await navigator.credentials.get({
  publicKey,
})) as PublicKeyCredential;
// 

2. The server verifies the assertion

verifyAuthenticationResponse() re-derives that same byte string from the response and checks the signature against the credential's stored public key — and that the signature counter didn't move backwards, which is one of the ways a cloned authenticator would get caught.

const result = await verifyAuthenticationResponse({
  response,
  expectedChallenge: challenge.challenge,
  expectedOrigin: rp.expectedOrigin,
  expectedRPID: rp.expectedRPID,
  credential: {
    id: credItem.credentialId,
    publicKey: isoBase64URL.toBuffer(credItem.publicKey),
    counter: credItem.counter,
    transports: credItem.transports as AuthenticatorTransportFuture[],
  },
});
// 

Try it

This needs a passkey already registered in your current demo session — head to Registration first if you haven't yet.