Skip to main content
The Pinaivu developer console (pinaivu-api/dashboard) authenticates users with Sui zkLogin. Instead of a seed phrase or a browser wallet, a developer signs in with their existing Google account and receives a real, self-custodied Sui address — derived from the OAuth identity through a zero-knowledge proof. The OAuth provider never learns the user’s Sui address, and Pinaivu never sees a private key. This page documents how that flow is wired end-to-end, the exact files and endpoints involved, and the security caveats worth knowing before you ship it.

Why zkLogin

A developer console needs an on-chain identity (so usage, keys, and payments can bind to a Sui address) but the audience is developers who do not want to manage a wallet just to create an API key. zkLogin closes that gap:
  • Seedless — the address is a function of the OAuth sub + a salt, proven in zero knowledge. There is no mnemonic to store or lose.
  • Self-custodied — every transaction is still authorised by an ephemeral key the browser generates and holds; the proof binds that ephemeral key to the user’s address for a bounded number of epochs.
  • Private — the link between the Google account and the Sui address is hidden behind the ZK proof. The chain sees a normal Sui address.
See Why Sui for why this primitive only exists on Sui.

Where it lives

The proving and salt-backed nonce are delegated to Enoki (Mysten Labs’ hosted zkLogin service); the OAuth provider is Google (response_type=id_token).

The flow

1

Initialize a session (/login)

ZkLoginService.initializeSession() mints a fresh ephemeral Ed25519 keypair, sends its public key to the Enoki nonce endpoint (NEXT_PUBLIC_ENOKI_NONCE_URL) with additionalEpochs: 2, and stores the returned { nonce, randomness, maxEpoch } plus the ephemeral private key in localStorage under pinaivu-zklogin-session.
maxEpoch is the Sui epoch after which both the nonce and any proof bound to this ephemeral key stop being valid — the practical session lifetime.
2

Redirect to Google

The browser is sent to Google’s OIDC endpoint requesting an id_token (a JWT) whose nonce claim is the value Enoki returned. The nonce cryptographically commits the JWT to this ephemeral key, so a stolen JWT can’t be replayed against a different key.
3

Fetch the ZK proof (/auth/callback)

Google redirects back with #id_token=<JWT> in the URL fragment. The callback recreates the ephemeral keypair from the saved session and asks Enoki to produce the zkLogin proof, passing the JWT in the zklogin-jwt header.
4

Derive the address and persist the proof

The zkLogin Sui address is derived from the JWT + salt (or from the proof’s addressSeed). The full proof bundle is cached in localStorage under pinaivu-zklogin-proof with a 24-hour TTL.
5

Link the account

The callback creates (or links) the Pinaivu account, binding the email and Sui address, then redirects into the console.

Signing Sui transactions

When the user later authorises an on-chain action, the ephemeral key signs the transaction bytes and ZkLoginService.createTransactionSignature wraps that signature together with the ZK proof and the addressSeed into a single zkLogin signature that Sui validators accept:
addressSeed comes straight from the proof when present, otherwise it is recomputed with genAddressSeed(BigInt(userSalt), "sub", sub, aud).

Session & route guarding

SessionManager (lib/zklogin/session.ts) keeps two localStorage keys: DashboardShell treats /login and /auth/callback as public and redirects every other route to /login when no valid cached proof exists. “Sign out” calls SessionManager.clearAll().

Environment variables

These are required by the dashboard for zkLogin to work. The NEXT_PUBLIC_ prefix means they are exposed to the browser (Enoki API keys used here are publishable client keys, not server secrets). Dependencies: @mysten/sui (/zklogin + /keypairs/ed25519) and jwt-decode.
Salt derivation is a development placeholder. The callback currently derives userSalt deterministically from the JWT sub with a simple non-cryptographic hash:
This is deterministic (the same Google account always maps to the same address) which is what you want, but it is not a production salt scheme: it has low entropy and the salt is recomputed client-side rather than held by a salt service. For production, back the salt with a persistent, per-user salt service (Enoki can manage this for you) so the OAuth↔address mapping cannot be brute-forced and stays stable across providers.
The id_token arrives in the URL fragment (#id_token=...), which is never sent to a server. The callback parses it client-side, derives the proof, then calls window.history.replaceState to strip it from the address bar. Keep the proving on the client (or a trusted edge) — never log the raw JWT.

Troubleshooting

  • “Google Client ID not configured.”NEXT_PUBLIC_GOOGLE_CLIENT_ID is unset at build time. NEXT_PUBLIC_ vars are inlined during the build, so rebuild after setting it.
  • Enoki nonce error / Enoki ZKP error — check the NEXT_PUBLIC_ENOKI_API_KEY and that the network matches the key’s configured network. The thrown error includes Enoki’s response body.
  • Redirect loops back to /login — the cached proof expired (24 h) or was cleared; sign in again. DashboardShell redirects whenever SessionManager.getProof() returns null.
  • No active session. in the callback — the user reached /auth/callback without first going through /login (the ephemeral session in localStorage is missing).

Why Sui

Why zkLogin and on-chain settlement are built on Sui specifically

Glossary

zkLogin, ephemeral key, salt, maxEpoch, and other terms