AuthenSeeDocs

Core concepts

Personas, providers, factors, and schemes — and how zero-knowledge proofs, Merkle trees, and nullifiers hold the model together.

AuthenSee authenticates users without ever seeing their secrets. This page defines the vocabulary the rest of the documentation uses, then explains the cryptographic machinery in plain terms.

The model in one diagram

                    one persona, many providers
                ┌───────────────────────────────────┐
                │            PERSONA                │
                │  the user's AuthenSee identity    │
                │                                   │
                │  enrolled schemes:                │
                │   • passkey_question_v1           │   factors stay
                │   • passkey_only_v1               │   on-device;
                │     (one commitment each)         │   the server holds
                └────────┬───────────────┬──────────┘   only commitments
                         │               │
              providerSubject A   providerSubject B
              externalUserId  A   externalUserId  B
                         │               │
                ┌────────┴─────┐  ┌──────┴───────┐
                │  PROVIDER A  │  │  PROVIDER B  │
                │  (your app)  │  │ (another app)│
                └──────────────┘  └──────────────┘

Terminology

These terms are used consistently across every page:

TermMeaning
PersonaA user's AuthenSee identity: one set of enrolled factors, reusable across every provider. Personas are created through a provider flow and have a type — human or agent.
ProviderYou — the app or business integrating AuthenSee. Identified by a provider ID; authenticated by a secret key (sk_...).
externalUserIdYour own user ID for an account, supplied when you mint a session. AuthenSee maps it to a persona.
providerSubjectAuthenSee's stable, provider-scoped alias for a persona — the sub of every auth-result JWT. Two providers get different subjects for the same persona, so users can't be correlated across providers.
FactorOne credential a user can enroll: a passkey, image points, a motion gesture, or an agent keypair.
SchemeA fixed factor combination an enrollment is registered against (for example passkey_question_v1 = image points + passkey), backed by a dedicated circuit. One commitment per enrolled scheme per persona.
Factor combinationYour provider policy: which scheme new users enroll and returning users must prove at login. One of passkey_and_image_points, passkey_only, passkey_and_behavior.
Hosted flowThe AuthenSee-operated web surface where ceremonies run — launched from your app as a popup or redirect.

Zero-knowledge proofs

A zero-knowledge proof (ZK proof) lets you prove you know something without revealing what you know.

Analogy: imagine a locked room with two doors. You can prove you have the key by entering through one door and exiting through the other — without ever showing anyone the key itself.

In AuthenSee, the "secret" is the user's factor responses — their image points or motion gesture, and their passkey signature. The ZK proof demonstrates that those responses reproduce the values committed during enrollment, without revealing the responses themselves.

What the proof proves

Each scheme is backed by a dedicated Noir circuit (see Circuit architecture). Depending on the scheme, a single proof demonstrates some or all of the following:

  1. Knowledge of the factor — the user knows the image points or motion gesture that, when hashed, reproduce the committed factor root (schemes that include a non-passkey factor)
  2. Merkle inclusion — the factor leaf is part of the tree committed during enrollment
  3. Passkey verification — the passkey signature is valid, verified inside the circuit, not as a separate step (every scheme except the agent keypair scheme)
  4. Challenge binding — the proof is bound to a specific server-issued challenge, preventing cross-provider hijacking

Each scheme aggregates its factors into a single auth_commitment field element at enrollment; a successful proof attests to all of the scheme's factors atomically, in one proof.

What the proof does not reveal

  • The answers themselves
  • Which specific prompts were answered
  • The individual leaf hashes
  • The Merkle tree structure or paths
  • Any salts used in hashing

The ceremony, end to end

User's device                                     Auth server
     |                                                 |
     |  ENROLL: hash factors on-device,                |
     |  aggregate into one commitment                  |
     |  ── commitment ───────────────────────────────► |  stores 1 field element
     |                                                 |
     |  LOGIN: request challenge                       |
     |  ◄── nonce + challengeBytes + layout ────────── |
     |                                                 |
     |  re-complete factors, generate ZK proof         |
     |  (secrets never leave local memory)             |
     |  ── proof + public inputs + nullifier ────────► |  verify proof,
     |                                                 |  claim nullifier,
     |  ◄── one-time authResultCode ────────────────── |  mint result

Merkle trees

A Merkle tree lets you commit to a set of values with a single hash (the "root") and later prove that any individual value is part of the set.

         Root
        /    \
      H01     H23
      / \     / \
    H0   H1 H2  H3
    |    |   |    |
   L0   L1  L2   L3

Each leaf (L0L3) is a hashed answer. Internal nodes hash their children together; the root is a single value that uniquely represents the entire set.

  • Enrollment: each answer is hashed into a leaf, the leaves are assembled into a tree, and only the root contributes to the commitment sent to the server.
  • Authentication: the user re-answers; the SDK reconstructs the leaves, computes an inclusion proof (the path from leaf to root), and feeds it into the ZK circuit.
  • Verification: the server checks that the proof's public root matches the stored commitment. It never sees leaves or paths.

Each persona has its own Merkle tree. Trees are never shared across users.

Poseidon2 hashing

AuthenSee hashes with Poseidon2 rather than SHA-256. Traditional hash functions require thousands of constraints inside a ZK circuit, making proof generation slow; Poseidon2 is ZK-native — it operates directly on the finite-field elements the proof system uses, requiring far fewer constraints.

Answers are hashed like this:

leaf = Poseidon2(Poseidon2(normalize(answer)), salt)
  1. The raw answer is normalized (lowercased, trimmed, whitespace-collapsed) for consistent hashing
  2. The normalized answer is hashed with Poseidon2
  3. The result is hashed again with a per-question salt, derived deterministically from the persona ID and question index
  4. The final value becomes a leaf in the Merkle tree

The double hash with salt prevents rainbow-table attacks and ensures the same answer produces different leaves for different questions.

Nullifiers

A nullifier prevents the same proof from being used twice:

nullifier = Poseidon2(salt, challengeId)
PropertyDescription
DeterministicThe same user + the same challenge always produce the same nullifier
UnlinkableDifferent challenges produce different nullifiers — proofs can't be correlated across sessions
Collision-resistantPoseidon2 over the BN254 scalar field provides ~254 bits of collision resistance
Single-useThe server stores spent nullifiers and rejects any proof that reuses one

Replay prevention, step by step:

  1. The server issues a challenge (nonce) for each authentication attempt
  2. The SDK computes the nullifier from the user's salt and the challenge ID
  3. The nullifier is a public input of the ZK proof
  4. The server checks whether the nullifier has been seen before
  5. Seen → reject (replay detected). New → claim it atomically and proceed

The check and insert happen in a single database transaction with a unique constraint as a safety net.

Linking an existing persona

A user who already enrolled with one provider can reuse that persona at a new provider instead of enrolling fresh factors — the "log in with your existing persona" path. The user proves any scheme their persona already holds, and the auth server links the persona to the new provider rather than creating a second one.

Because passkeys can't be enumerated across origins, the hosted flow surfaces this as a prompt rather than a lookup — and can hand the ceremony to a second device (scan a QR code, prove there) when the current device doesn't hold the right passkey. Either way, completion reaches your callback with linked and providerSubject present instead of a fresh authResultCode. See the hosted pages guide for the flow and provider-links for the API.

Provider-specific challenges

Every authentication proof is bound to a specific provider via a server-generated challenge, preventing cross-provider hijacking — a proof generated for provider A cannot be replayed against provider B.

The server identifies the provider from the session token, generates a challenge with provider-specific metadata (provider ID, domain, timestamp, nonce), and returns it to the SDK. The challenge becomes a public input in the ZK proof and is verified during proof verification.

Circuit architecture

AuthenSee uses one Noir circuit per scheme:

SchemeCircuitFactors verifiedSelected by
passkey_question_v1passkey_question_authImage points + passkeyPolicy passkey_and_image_points (default)
behavior_passkey_v1behavior_auth_passkeyMotion gesture + passkeyPolicy passkey_and_behavior
passkey_only_v1passkey_only_authPasskey onlyPolicy passkey_only, or opportunistically alongside any other scheme
agent_keypair_v1agent_keypair_authRaw ECDSA-P256 keypair (no WebAuthn)Agent personas only

The agent_keypair_v1 scheme is restricted to personas flagged as agent, preventing humans from downgrading to weaker authentication. A challenge is bound to one scheme (resolved from the enrollment), and the server rejects any proof that doesn't match that scheme's circuit and expected public-input layout.

Whether agent personas may authenticate at all — and how fast — is each provider's call: your policy can block agents outright or cap each agent at a per-minute budget, enforced by the auth server at enrollment, challenge binding, and verification. See Provider policy.

What the server stores

DataDescription
Aggregate scheme commitmentsOne field element per enrolled scheme per persona
Spent nullifiersPrevents proof replay
Proof metadataProver version and timestamps only (for analytics)
Provider configsAPI keys, branding, auth policies
Persona IDsOpaque identifiers with type (human/agent)

The server does not store: full proofs, answer hashes, questions, answers, salts, Merkle paths, or any PII by default. End users manage their identity — devices, providers, activity — at their own AuthenSee account page; nothing there is exposed to providers.

Next

On this page