Authentication
Authenticate users with ZK proofs. Handle success, failure, and debug mode.
Authentication
Authentication in AuthenSee works through a hosted challenge-response protocol backed by zero-knowledge proofs. The user completes the passkey and factor ceremony in the AuthenSee-hosted runtime; your backend receives only a one-time auth-result code and exchanges it for provider-scoped claims.
This is the on-device engine, not a third-party integration path.
authenticate()runs the WebAuthn passkey ceremony, which only succeeds on AuthenSee's own origin (the hosted flow and first-party apps). To authenticate users from your own app, launch the hosted popup — see the embed guide. This page documents how authentication works inside that flow.
V1 ships exactly one scheme: passkey_question_v1, backed by the passkey_question_auth circuit. Every successful authentication attests to both a security-question answer and a WebAuthn ECDSA-P256 passkey signature in a single ZK proof, and the JWT's factorsVerified is server-derived from the scheme — always ["security_questions", "passkey"]. Clients no longer send factorType or circuitType to /v1/verify.
Authentication with hosted flow
On your backend, exchange the one-time result code returned to your callback URL:
The browser passkey ceremony is handled by the hosted runtime. Provider code
does not receive personaId, challengeId, commitments, public inputs, or
session tokens.
The authentication flow
When authenticate() is called, the SDK executes:
-
Look up the on-device enrolment witness. The witness lives in sessionStorage and contains the question Merkle path + salts + passkey material along with the
enrollmentIdreturned by the server at enrol time. -
Request a challenge —
POST /v1/challengeswith{ personaId, enrollmentId }. The server returns{ challengeId, nonce, challengeBytes (32 random bytes), publicInputLayout: { authCommitmentIndex, challengeFieldIndex, nullifierIndices, totalLength }, schemeId, factors }. -
Collect the answer + run the WebAuthn ceremony. The SDK invokes the
passkey.signcallback with the server'schallengeBytes; the callback runsnavigator.credentials.get(...)and returns the parsed assertion. -
Build the witness — 100 flat public inputs (
auth_commitment,challenge_field,challenge_bytes[32],action_hash,expected_rp_id_hash[32],expected_origin_hash[32],auth_nullifier) plus the private witness (clientDataJSON, authenticatorData, signature, pubkey, Merkle path, salts).passkey_commitmentis no longer public — the circuit recomputes it internally from the private witness. -
Generate ZK proof — On-device via WasmProver (bb.js UltraHonk). The circuit asserts:
- ECDSA-P256 verifies against the passkey pubkey
- clientDataJSON binds the server's
challengeBytesand the expected origin - Merkle path lifts the answer leaf to the enrolled
question_root - The recomputed
passkey_commitment+question_roottogether hash to the enrolledauth_commitment
Wrong answer or wrong passkey → assertion failure inside Barretenberg →
INVALID_PROOFsurfaced locally, no server round-trip. -
Submit proof —
POST /v1/verifywith{ challengeId, personaId, proof, publicInputs, nullifiers }. The SDK extractsnullifiersusing the indices the server published inchallenge.publicInputLayout.nullifierIndices— clients never hardcode public-input positions. -
Server verification — Resolves the scheme from the challenge's stored
schemeId, looks up the verifier circuit + expected layout from the registry, asserts strict public-input bindings (V1:auth_commitmentat 0,challenge_fieldat 1,auth_nullifierat 99), verifies the proof against the scheme's pinned VK, atomically claimsauth_nullifier, and issues the JWT withfactorsVerifiedderived from the scheme registry.
The AuthResult type
Handling errors
When authentication fails, the error field contains a structured error code:
Error codes reference
| Code | Description |
|---|---|
INVALID_PROOF | ZK proof verification failed on the server |
FACTOR_NOT_ENROLLED | Attempted to authenticate with a factor that is not enrolled |
CHALLENGE_EXPIRED | Authentication challenge has expired (server-side TTL, default 5 minutes) |
MERKLE_ROOT_STALE | Proof references an outdated Merkle root (factor was updated) |
NETWORK_ERROR | Network request failed (proof generation still works offline) |
USER_CANCELLED | User cancelled the authentication flow |
Debugging
Provider backends should log only provider-scoped session ids, auth-result ids, and error codes. Do not log callback codes, provider secret keys, or issued tokens. Runtime proof-generation diagnostics are owned by the hosted flow.
Offline support
Proof generation works entirely offline. The network is only required for:
- Challenge retrieval -- Fetching a fresh nonce from the server before authentication.
- Proof submission -- Sending the generated proof to the server for verification.
If the device is offline, the SDK queues the proof and submits it when connectivity is restored.
Performance targets
| Metric | Target |
|---|---|
| End-to-end auth time (headline KPI) | P50: 5 seconds, P95: 10 seconds |
| Server-side verification | P99: < 500ms |