AuthenSeeDocs

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:

import { createAuthenSeeSdk } from '@rebellion-systems/authensee-sdk';
 
const authensee = createAuthenSeeSdk({
  serverUrl: 'https://api.authensee.com',
  apiKey: process.env.AUTHENSEE_SECRET_KEY!,
});
 
const result = await authensee.exchangeAuthResult(authResultCode);
console.log(result.providerSubject, result.factorsVerified, result.token);

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:

  1. Look up the on-device enrolment witness. The witness lives in sessionStorage and contains the question Merkle path + salts + passkey material along with the enrollmentId returned by the server at enrol time.

  2. Request a challengePOST /v1/challenges with { personaId, enrollmentId }. The server returns { challengeId, nonce, challengeBytes (32 random bytes), publicInputLayout: { authCommitmentIndex, challengeFieldIndex, nullifierIndices, totalLength }, schemeId, factors }.

  3. Collect the answer + run the WebAuthn ceremony. The SDK invokes the passkey.sign callback with the server's challengeBytes; the callback runs navigator.credentials.get(...) and returns the parsed assertion.

  4. 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_commitment is no longer public — the circuit recomputes it internally from the private witness.

  5. 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 challengeBytes and the expected origin
    • Merkle path lifts the answer leaf to the enrolled question_root
    • The recomputed passkey_commitment + question_root together hash to the enrolled auth_commitment

    Wrong answer or wrong passkey → assertion failure inside Barretenberg → INVALID_PROOF surfaced locally, no server round-trip.

  6. Submit proofPOST /v1/verify with { challengeId, personaId, proof, publicInputs, nullifiers }. The SDK extracts nullifiers using the indices the server published in challenge.publicInputLayout.nullifierIndices — clients never hardcode public-input positions.

  7. 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_commitment at 0, challenge_field at 1, auth_nullifier at 99), verifies the proof against the scheme's pinned VK, atomically claims auth_nullifier, and issues the JWT with factorsVerified derived from the scheme registry.

The AuthResult type

type AuthResult = {
  success: boolean;
  token?: string;       // JWT from the auth server
  claims?: {
    personaId: string;
    providerId: string;
    personaType: 'human' | 'agent';
    factorsVerified: FactorType[];
    agentId?: string;   // present for agent personas
    iat: number;
    exp: number;
  };
  error?: AuthError;
};

Handling errors

When authentication fails, the error field contains a structured error code:

const result = await AuthenSee.authenticate();
 
if (!result.success) {
  switch (result.error.code) {
    case 'INVALID_PROOF':
      // ZK proof verification failed -- user likely answered incorrectly
      console.log('Incorrect answers. Please try again.');
      break;
    case 'FACTOR_NOT_ENROLLED':
      // The required factor is not enrolled -- prompt enrollment
      await AuthenSee.enroll('security_questions');
      break;
    case 'CHALLENGE_EXPIRED':
      // Challenge TTL exceeded (default 5 minutes) -- retry
      const retryResult = await AuthenSee.authenticate();
      break;
    case 'MERKLE_ROOT_STALE':
      // Factor was updated from another device -- re-sync
      console.log('Your factors were updated. Please try again.');
      break;
    case 'NETWORK_ERROR':
      // Network request failed -- proof generation still works offline
      console.log('Network error. Proof will be submitted when online.');
      break;
    case 'USER_CANCELLED':
      // User dismissed the authentication UI
      break;
  }
}

Error codes reference

CodeDescription
INVALID_PROOFZK proof verification failed on the server
FACTOR_NOT_ENROLLEDAttempted to authenticate with a factor that is not enrolled
CHALLENGE_EXPIREDAuthentication challenge has expired (server-side TTL, default 5 minutes)
MERKLE_ROOT_STALEProof references an outdated Merkle root (factor was updated)
NETWORK_ERRORNetwork request failed (proof generation still works offline)
USER_CANCELLEDUser 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:

  1. Challenge retrieval -- Fetching a fresh nonce from the server before authentication.
  2. 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

MetricTarget
End-to-end auth time (headline KPI)P50: 5 seconds, P95: 10 seconds
Server-side verificationP99: < 500ms

On this page