Registration gave you a public key. Authentication is the proof that the same private key is still on the other side.
The exchange
- Your server issues request options: a fresh challenge, and optionally the list of credentials you accept.
- The authenticator signs the challenge and the authenticator data.
- Your server verifies the signature against the stored public key, and checks the challenge, the origin, the relying party hash and the flags.
What to store, and why
- The credential identifier. How you find the record when a credential presents itself.
- The public key, in COSE form. Keep the original bytes: re-encoding CBOR breaks signatures.
- The sign counter. See below.
- The user handle, to support username-less sign-in.
- A label and a creation date, so the user can recognise and revoke a device.
That last one is not cosmetic. Somebody who loses a laptop needs to remove the right credential without removing all of them.
The sign counter, handled sanely
An authenticator may increment a counter at each use, so a server can spot a cloned key: a value lower than the one stored means two copies exist.
Synchronised passkeys generally report zero and never move. Treat a counter of zero as « not supported », not as an alarm, or you will lock out the majority of your users. Enforce the check only when the stored value is already non-zero.
Conditional UI
Rather than a « sign in with a passkey » button, the browser can offer the credential inside the login field itself, alongside saved passwords. It requires discoverable credentials, so ask for residentKey: preferred at registration if you want that later.