Server Verification

Server Verification

⚠️

The SDK only runs the on-device ceremony. A passkey is trustworthy only after your backend verifies it. Never trust the client result on its own.

The flow

  1. Server generates options. Your backend creates the options JSON with a fresh, single-use challenge (plus RP ID, user, and allowed credentials).
  2. Client runs the ceremony. Pass that JSON to create / authenticate. The platform authenticator produces a response.
  3. Client returns rawJson. POST result.value.rawJson back to your server.
  4. Server verifies. Validate the challenge, origin, RP ID, signature, and sign-count against what you issued, then store (registration) or accept (authentication) the credential.

Use a maintained WebAuthn server library

Don’t hand-roll verification. rawJson carries every field these libraries expect:

What the server checks

A correct verification confirms, at minimum:

  • The challenge matches the one you issued and hasn’t been used before.
  • The origin and RP ID match your domain.
  • The signature is valid for the stored credential public key.
  • The sign-count advanced (clone detection), where the authenticator reports one.

Only after these pass should you treat the user as registered or authenticated.

Two checks no library can do for you: which account a registration is allowed to enroll a passkey for (bind it to your signed-in session or sign-up flow — a successful ceremony proves possession of an authenticator, not ownership of an account), and the origin list, which must include every exact origin your clients report (ports included, and android:apk-key-hash:<sha256> for native Android apps).

Using passkeys-server

On a Kotlin/JVM backend, io.github.androidpoet:passkeys-server wraps java-webauthn-server behind PasskeyRelyingParty + passkeyRoutes(relyingParty, enrollment), with expiring single-use ceremonies, exact-origin matching, a server-enforced user-verification policy and compare-and-set counter updates. See the README’s Server (Relying Party) section.