POST /v1/auth/passkey/login/complete
Finalize a WebAuthn authentication ceremony. Issues a browser session on
success — AAL2, auth_method: "passkey".
::: tip Auth Required: none. The credential IS the credential. :::
Request
POST /v1/auth/passkey/login/complete
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | yes | — |
Body
{
"credential": { /* PublicKeyCredential from navigator.credentials.get() */ }
}
Response
200 OK
A full LoginResponse::Success body. Cookies set: identsphere_at, identsphere_rt,
identsphere_csrf. The auth_method field is "passkey" and mfa_verified_at
is set to the current time.
Error responses
| Status | Code | When |
|---|---|---|
| 401 | authentication_required | No matching pending challenge, ceremony failed validation, credential isn't in the database, or the account is disabled. |
| 500 | internal_error | Serialization, DB, or JWT failure. |
Example (manual)
const opts = await fetch('/v1/auth/passkey/login/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'alice@example.com' }),
}).then(r => r.json());
const cred = await navigator.credentials.get({ publicKey: opts.publicKey });
const res = await fetch('/v1/auth/passkey/login/complete', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: cred }),
});
@identsphere/react's usePasskeyLogin() wraps both steps into a single
mutation.
Notes
- Sign counter +
last_used_atare bumped on the matcheduser_passkeysrow. - Challenge state is deleted regardless of outcome.
- An audit entry (
auth.passkey.login) is recorded. - Disabled / deleted accounts can't sign in even with a valid passkey.