Skip to main content

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

HeaderRequiredNotes
Content-Type: application/jsonyes

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

StatusCodeWhen
401authentication_requiredNo matching pending challenge, ceremony failed validation, credential isn't in the database, or the account is disabled.
500internal_errorSerialization, 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_at are bumped on the matched user_passkeys row.
  • 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.