AuthenSeeDocs

Authentication

Authenticate users with ZK proofs. Handle success, failure, and debug mode.

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.

Every enrollment is registered against one scheme, and a successful authentication attests to that scheme's factors in a single ZK proof:

SchemeFactorsSelected by provider policy
passkey_question_v1Image points + passkeypasskey_and_image_points (default)
behavior_passkey_v1Motion gesture + passkeypasskey_and_behavior
passkey_only_v1Passkey onlypasskey_only, or opportunistically alongside any other scheme
agent_keypair_v1Raw ECDSA-P256 keypair (no WebAuthn)Agent personas only

The auth server resolves the scheme from the challenge and derives factorsVerified from it — clients never send factorType or circuitType to /v1/verify. A provider's factor combination determines which scheme is allowed at login; see Provider policy for what happens when a user's enrolled scheme falls outside the provider's current policy.

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

The walkthrough below uses passkey_question_v1 (image points + passkey) as the concrete example; behavior_passkey_v1 and passkey_only_v1 follow the same shape with their own factor (or none) in place of the answer, and agent_keypair_v1 skips the WebAuthn ceremony entirely in favor of a raw ECDSA-P256 signature.

When authenticate() is called, the SDK executes:

  1. Look up the on-device enrolment witness. The witness lives in sessionStorage and contains the image-points 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 (for passkey_question_v1: auth_commitment at 0, challenge_field at 1, auth_nullifier at 99 — each scheme has its own layout), 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('image_points');
      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
INCORRECT_ANSWERThe image-points answer didn't match what was enrolled (surfaced locally, before any server round-trip)
INCORRECT_MOTIONThe motion gesture didn't match the enrolled template
PROVER_INIT_FAILEDThe on-device WASM prover failed to initialize
PROOF_GENERATION_FAILEDProof generation failed for a reason other than an incorrect factor
FACTOR_ERRORGeneric factor-collection error (e.g. malformed input)
PERSONA_ALREADY_ENROLLEDBootstrap enrollment attempted for a persona that already has a current enrollment

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 timeP50: 5 seconds, P95: 10 seconds
Server-side verificationP99: < 500ms

Next

  • Enrollment — what the persona proved control of, and how it got there
  • Events — lifecycle events for progress UI and analytics
  • Verification reference — every server-side check on /v1/verify

On this page