---
title: "Verify every passkey answer on the server · Agent Wranglers"
url: https://agentwranglers.se/playbook/accounts/sign-in-methods/passkey-verification
updated: 2026-10-20
licence: "All rights reserved"
---

# Verify every passkey answer on the server

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Sign-in methods](https://agentwranglers.se/playbook/accounts/sign-in-methods)
- Rule: Accept a passkey sign-in only after the server checks the challenge, origin, relying party ID, user flags and signature with a maintained WebAuthn library.
- How: Make a fresh challenge of at least 16 random bytes for each ceremony, store it server-side for at most 5 minutes and use it once. Pass the expected challenge, every allowed origin and the relying party ID to the library, such as SimpleWebAuthn or py_webauthn. Require user verification when the passkey is the only factor. Look up the credential by its ID and check it belongs to the account it names. After success, save the new signature counter and backup state. A counter that goes down on a key that counts is a sign of a cloned key, so flag it. Synced passkeys usually report 0, which is fine.
- Check: Tests replay a captured sign-in response, send one with a changed origin and send one signed for another account's credential ID. Each is refused, and a fresh response is accepted once.

Example:

```js
import { verifyAuthenticationResponse } from '@simplewebauthn/server';

export async function finishPasskeySignIn(db, session, response) {
  const challenge = await db.challenges.takeOnce(session.id);
  const passkey = await db.passkeys.findById(response.id);
  if (!challenge || !passkey) throw new UnauthorizedError();
  const { verified, authenticationInfo } = await verifyAuthenticationResponse({
    response,
    expectedChallenge: challenge,
    expectedOrigin: 'https://example.com',
    expectedRPID: 'example.com',
    credential: { id: passkey.id, publicKey: passkey.publicKey, counter: passkey.counter, transports: passkey.transports },
    requireUserVerification: true,
  });
  if (!verified) throw new UnauthorizedError();
  await db.passkeys.recordUse(passkey.id, authenticationInfo.newCounter, authenticationInfo.credentialBackedUp);
  return passkey.userId;
}
```

```python
from webauthn import verify_authentication_response
from webauthn.helpers import base64url_to_bytes

def finish_passkey_sign_in(db, session_id: str, response: dict) -> str:
    challenge = db.challenges.take_once(session_id)
    passkey = db.passkeys.find_by_id(base64url_to_bytes(response["id"]))
    if challenge is None or passkey is None:
        raise Unauthorized()
    result = verify_authentication_response(
        credential=response,
        expected_challenge=challenge,
        expected_rp_id="example.com",
        expected_origin="https://example.com",
        credential_public_key=passkey.public_key,
        credential_current_sign_count=passkey.sign_count,
        require_user_verification=True,
    )
    db.passkeys.record_use(passkey.id, result.new_sign_count, result.credential_backed_up)
    return passkey.user_id
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers accounts playbook: https://agentwranglers.se/playbook/accounts/sign-in-methods/passkey-verification`.

- Previous: [Offer passkeys first](https://agentwranglers.se/playbook/accounts/sign-in-methods/passkeys-first)
- Next: [Keep the password manager's passkey list current](https://agentwranglers.se/playbook/accounts/sign-in-methods/passkey-signals)
- [Sign-in methods](https://agentwranglers.se/playbook/accounts/sign-in-methods)
