A login form asks for a password, sends it over TLS, and the server compares it to a stored hash. If the database leaks, that hash becomes an offline cracking target, and anyone who reused the password is exposed elsewhere. Zero-knowledge proofs change the shape of the exchange: the browser proves it can satisfy a statement — "I know the private key behind this public key" — without sending the key or a reusable derivative of it.
What follows is the practical version for a web app: key generation at signup, a challenge-response login, server-side verification, and the mistakes that quietly undo the privacy you were trying to get.
Strip away the cryptography and the flow looks ordinary. At registration the client generates a keypair and sends only the public half. At login the server sends a random challenge, the client uses the private key to produce a proof, and the server checks that proof against the stored public key. If it verifies, you issue a session cookie like any other login.
The privacy gain is narrow but real: no password crosses the wire, no password hash sits in your database, and a proof captured from one session is useless in the next. That last property is what separates this from simply posting a static token. The value the client sends is bound to a fresh, server-chosen nonce.
It helps to know that familiar schemes already fit this description. A Schnorr signature is a non-interactive zero-knowledge proof of knowledge of a discrete logarithm; Fiat–Shamir turns an interactive exchange into a single message by hashing the commitment and the context into the challenge. So "using ZK for login" usually means using a well-tested signature scheme correctly, not inventing a circuit.
Two families cover most authentication needs, and their costs differ sharply.
If all you want is passwordless, WebAuthn and passkeys already give you hardware-backed keypairs and a browser-mediated ceremony. Adopt them unless you have a specific reason not to. Circuits enter the picture when the verifier must learn nothing beyond a claim, or when you cannot depend on platform authenticators.
The server starts by issuing a nonce and remembering it — in a signed cookie, a short-lived cache entry, or a database row. It must be unpredictable, single-use, and expire within a minute or two.
The client then produces a proof bound to that nonce and to the relying origin:
Commitment: R = r·G. Challenge: c = H(R, publicKey, nonce, origin). Response: s = r + c·x. Server check: s·G == R + c·publicKey.
A library will handle the hash-to-scalar step and the encoding for you. What matters is what goes into that hash. Include the origin and any session context, so a proof generated for one site or one flow cannot be replayed in another. Domain separation is not optional.
The server verifies the proof, marks the nonce as spent, and issues a normal session token. From that point on, your authorization code does not need to know a zero-knowledge proof was involved.
Passwordless login is the easy case. The more interesting problems appear when the verifier must not learn the underlying fact:
Each of these needs a circuit, a setup decision for some proving systems, and a verification key deployed next to your backend. That is real work. Budget for it instead of promising it inside a sprint.
Start with passwordless login built on a standard signature scheme. Add recovery codes and a second device during registration, since key loss is the failure mode users actually hit. Write tests for replayed proofs, expired nonces, and proofs generated for the wrong origin — those three cases catch most real bugs. Keep the private key on the client, keep the public key on the server, and log nothing that identifies the credential. Later, when you have one specific privacy claim to make, add a circuit for that claim alone. That order ships the easy win now and leaves the harder cryptography the review time it deserves.