Part of #1.
Blocked by: #13 (Passkeys: registration)
Problem. Passkey login must work both with a known user (email first) and discoverable credentials (no email, the authenticator picks), and must update the signature counter to detect cloned authenticators.
Proposed approach.
POST /passkeys/login/options with an optional email: returns PublicKeyCredentialRequestOptions with a fresh challenge row; when an email is given, allowCredentials lists that user's passkeys; when not, it is empty and the authenticator offers discoverable credentials.
POST /passkeys/login/verify: consumes the challenge, looks up the credential by id, verifies the assertion signature over authenticatorData || sha256(clientDataJSON) with the stored COSE key, checks userPresent, checks the sign count is greater than stored when both are non-zero (a regression marks the credential suspect and refuses), stores the new count, and issues a session.
- Conditional UI (autofill) support: the options endpoint tolerates an empty request so the browser can pre-fetch.
Acceptance criteria
Part of #1.
Blocked by: #13 (Passkeys: registration)
Problem. Passkey login must work both with a known user (email first) and discoverable credentials (no email, the authenticator picks), and must update the signature counter to detect cloned authenticators.
Proposed approach.
POST /passkeys/login/optionswith an optional email: returnsPublicKeyCredentialRequestOptionswith a fresh challenge row; when an email is given,allowCredentialslists that user's passkeys; when not, it is empty and the authenticator offers discoverable credentials.POST /passkeys/login/verify: consumes the challenge, looks up the credential by id, verifies the assertion signature overauthenticatorData || sha256(clientDataJSON)with the stored COSE key, checksuserPresent, checks the sign count is greater than stored when both are non-zero (a regression marks the credentialsuspectand refuses), stores the new count, and issues a session.Acceptance criteria