Get started
Quick start
An experimental Proof V2 lifecycle in four calls. Registration carries verifier material; login carries a fresh proof and public signals, never the raw password.
Proof V2 Lab
This flow requires the published 2.0 beta and explicit matching development proving keys. Keep it disabled in production pending independent cryptographic review and a production ceremony. Install
zkauth-client and set your keys. Installation covers it in a minute.Register a user
The client derives a password-based commitment locally and sends the salt plus commitment; the engine stores that verifier material.
ts
import { zkauth } from './lib/zkauth'
await zkauth.register({ email: 'ada@example.com', password: 'correct horse battery staple', deviceInfo: { deviceName: 'Chrome on Mac', deviceType: 'desktop' },})Log in
Login repeats the handshake. The engine verifies the proof, applies replay protection, and returns a session token.
ts
const res = await zkauth.login({ email: 'ada@example.com', password: 'correct horse battery staple', deviceInfo: { deviceName: 'Chrome on Mac', deviceType: 'desktop' },})
// The token lives on the instance; persist it in an http-only cookie too.const token = res.data.session.tokenRead the current user
The client keeps the session after login, so read the current user with no arguments.
ts
const user = await zkauth.getCurrentUser() // uses the active sessionconsole.log(user?.email)Log out
Invalidate the session when the user signs out.
ts
await zkauth.logout()Handle callbacks and hosted handoff
Set a primary redirect URL in the dashboard, then add it to the allowlist.
- ZKAuth sends users there after email verification, device approval, device denial, and password reset links.
- If no safe redirect is configured, ZKAuth shows a hosted fallback page instead of redirecting to an unknown URL.
- Hosted sign-up/sign-in helper pages and hosted forgot-password/reset pages can run through the project-bound hosted proxy without exposing project API keys.
- Hosted sign-in can return a single-use handoff code to an allowlisted callback. Redeem it from your backend with the project API key, then create your app session cookie without exposing the bearer token in browser storage or callback URLs.
- Display callback errors only through allowlisted copy, not raw query text.
- After engine sign-in, hosted UI can show a memory-only account security helper for profile, account stats, recent activity, sessions, devices, and recovery.
- That helper also covers shown-once recovery-code export, TOTP setup/reset, MFA disable, hosted-origin passkeys, and user API-key helper controls.
- Fresh-session guarded actions, such as terminating other active sessions, still require the engine to confirm a recent first-factor session.
- Configure exact browser origins for your production and preview domains; origins must not include paths or wildcards.
ts
import { randomBytes, timingSafeEqual } from 'node:crypto'import { redeemZKAuthHostedHandoff } from '@zkauth/node'
const stateCookie = 'zkauth_auth_state'
function sameState(received: string | null, expected: string | null) { if (!received || !expected) return false const left = Buffer.from(received, 'utf8') const right = Buffer.from(expected, 'utf8') return left.length === right.length && timingSafeEqual(left, right)}
function readCookie(request: Request, name: string) { for (const part of (request.headers.get('cookie') || '').split(';')) { const [key, ...value] = part.trim().split('=') if (key === name) return value.join('=') || null } return null}
export async function startHostedSignIn(request: Request) { const state = randomBytes(32).toString('base64url') const hosted = new URL( '/hosted/' + encodeURIComponent(loadProjectSlug()) + '/sign-in', loadHostedOrigin(), ) hosted.searchParams.set('state', state)
return new Response(null, { status: 303, headers: { Location: hosted.toString(), 'Set-Cookie': stateCookie + '=' + state + '; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=600', }, })}
const callbackErrorCopy: Record<string, string> = { invalid_token: 'The authentication link is invalid or expired.', token_expired: 'The authentication link is invalid or expired.', token_consumed: 'The authentication link is invalid or expired.', device_denied: 'The device request was denied.',}
function safeCallbackError(error: string | null) { return error && callbackErrorCopy[error] ? callbackErrorCopy[error] : 'The authentication action could not be completed. Try again.'}
export async function GET(request: Request) { const url = new URL(request.url) const params = url.searchParams const action = params.get('zkauth_action') const code = params.get('code')
if (code) { const state = params.get('state') const expectedState = readCookie(request, stateCookie) const clearState = stateCookie + '=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0' if (!sameState(state, expectedState)) { return new Response('Hosted sign-in state is invalid or expired.', { status: 401, headers: { 'Set-Cookie': clearState }, }) }
const handoff = await redeemZKAuthHostedHandoff( { apiKey: loadServerOnlyProjectKey(), baseUrl: loadServerOnlyApiBaseUrl(), }, code, )
const response = new Response(null, { status: 303, headers: { Location: new URL('/app', request.url).toString() }, }) response.headers.append('Set-Cookie', clearState) response.headers.append( 'Set-Cookie', 'zkauth_session=' + encodeURIComponent(handoff.sessionToken) + '; Path=/; HttpOnly; Secure; SameSite=Lax', ) return response }
if (action === 'password_reset') { return showResetPasswordForm(params.get('token')) }
if (params.get('success') === 'true') { return Response.redirect(new URL('/app', request.url)) }
return showAuthError(safeCallbackError(params.get('error')))}Lab complete
You completed the experimental proof flow. This verifies the integration path, not production approval. Next, compare supported methods in Authentication, or wire a framework in Examples.