Skip to main content

zkLogin Integration Guide

To implement zkLogin, first read What is zkLogin? for the underlying concepts, then configure at least one OpenID provider.

Your application generates an ephemeral key pair, then prompts the user to complete an OAuth login flow with a nonce derived from the ephemeral public key. When the provider returns the JSON Web Token (JWT), your application obtains a zero-knowledge proof and a unique user salt, then computes the zkLogin Sui address from the OAuth subject identifier and that salt. Your application signs transactions with the ephemeral private key and submits each transaction with both the ephemeral signature and the zero-knowledge proof.

Production hardening

Harden your integration before you move to production. Run a production salt service with proper availability and backups, because a user who loses a salt permanently loses access to that address. Serve all endpoints over HTTPS and configure Cross-Origin Resource Sharing (CORS) so that only your origins reach your salt and proving backends. Bind the OAuth nonce to both the JWT and the ephemeral public key, and validate it on return to prevent replay and token-substitution attacks. Never log JWTs, zero-knowledge proofs, or salts, because these values are sensitive credentials. Keep separate Testnet and Mainnet configuration for your OAuth clients, salt service, and prover so that test credentials never reach production.

See Security Best Practices.

Install the zkLogin TypeScript SDK

To use the zkLogin TypeScript SDK in your project, run the following command in your project root:

$ npm install @mysten/sui

If you want to use the latest experimental version:

$ npm install @mysten/sui@experimental

Step 1: Generate an ephemeral key pair and nonce

Generate a short-lived key pair, set an expiration epoch, and compute a nonce that binds the key to the OAuth session.

import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { generateNonce, generateRandomness } from '@mysten/sui/zklogin';
import { SuiGrpcClient } from '@mysten/sui/grpc';

const FULLNODE_URL = 'https://fullnode.testnet.sui.io:443';
const suiClient = new SuiGrpcClient({ baseUrl: FULLNODE_URL, network: 'testnet' });
const { systemState } = await suiClient.core.getCurrentSystemState();
const { epoch } = systemState;

const maxEpoch = Number(epoch) + 2; // ephemeral key valid for 2 epochs from now
const ephemeralKeyPair = new Ed25519Keypair();
const randomness = generateRandomness();
const nonce = generateNonce(ephemeralKeyPair.getPublicKey(), maxEpoch, randomness);

The maxEpoch controls how long this ephemeral key can authorize transactions. The address itself does not expire; only the session does. When the epoch passes maxEpoch, generate a new key pair and zero-knowledge proof. See Does my address expire? for details.

Step 2: Direct the user to the OAuth login

Construct the OAuth URL with your configured client ID, redirect URL, and the nonce from the previous step.

https://accounts.google.com/o/oauth2/v2/auth
?client_id=$CLIENT_ID
&response_type=id_token
&redirect_uri=$REDIRECT_URL
&scope=openid
&nonce=$NONCE

The JWT is returned directly in the redirect URL (id_token parameter). No token exchange is needed.

Step 3: Decode the JWT

After the OAuth redirect, the JWT appears as a URL parameter. For providers that return it directly, it looks like this:

https://your-app.com/auth?id_token=tokenPartA.tokenPartB.tokenPartC

Decode the JWT to extract the claims you need. The Sui SDK provides a built-in decodeJwt utility:

import { decodeJwt } from '@mysten/sui/zklogin';

const decodedJwt = decodeJwt(encodedJWT);
// decodedJwt.sub — the user's unique identifier
// decodedJwt.iss — the OAuth provider
// decodedJwt.aud — your application's client ID

You can validate the structure of the encoded JWT by pasting it in jwt.io.

Step 4: Manage user salt

The user salt is a 16-byte value (or integer smaller than 2n**128n) that, combined with the OAuth subject identifier, determines the user's Sui address. If a user loses their salt, they permanently lose access to that address.

Choose a salt management strategy:

StrategyDescriptionTrade-offs
User-providedAsk the user to enter and remember the salt.Basic but poor UX; users forget salts.
Browser/device storageStore in localStorage or mobile secure storage.Easy but lost on device/browser change. Email the salt to the user as a backup.
Database mappingMap user identifier (sub) to a salt in your backend database.Reliable; requires a backend service. Salt is unique per user.
Derived from master seedUse HKDF(ikm = seed, salt = iss || aud, info = sub) to derive salts deterministically from a master seed.No per-user storage needed, but the master seed cannot be rotated and the client ID (aud) cannot change without deriving a different address.

The Mysten Labs Enoki platform provides a managed salt service (using derived salts). Contact Mysten Labs for access. Only JWTs authenticated with allowlisted client IDs are accepted.

curl -X POST https://salt.api.mystenlabs.com/get_salt \
-H 'Content-Type: application/json' \
-d '{"token": "$JWT_TOKEN"}'

# Response: {"salt":"129390038577185583942388216820280642146"}

The salt's purpose is to disconnect the OAuth identifier (sub) from the onchain Sui address. Leaking the salt enables linking Web2 and Web3 identities but does not compromise fund control.

Step 5: Derive the user's Sui address

With the JWT and salt, derive the zkLogin address:

import { jwtToAddress } from '@mysten/sui/zklogin';

// The third argument (legacyAddress) is required.
// Pass false for new integrations; pass true only for addresses created before the derivation fix.
const zkLoginUserAddress = jwtToAddress(jwt, userSalt, false);

The same sub + iss + aud + user_salt always produces the same address. See Address definition for the full derivation formula.

Step 6: Get the zero-knowledge proof

The zero-knowledge proof attests that the ephemeral key pair is valid and bound to the user's OAuth identity, without revealing the JWT onchain.

First, generate the extended ephemeral public key:

import { getExtendedEphemeralPublicKey } from '@mysten/sui/zklogin';

const extendedEphemeralPublicKey = getExtendedEphemeralPublicKey(
ephemeralKeyPair.getPublicKey(),
);

You need a new zero-knowledge proof whenever the ephemeral key pair expires or is lost. The proof can be reused for multiple transactions within the same session.

Option A: use the Mysten Labs proving service

For Mainnet access, refer to the Enoki docs and contact Mysten Labs. For development, use the public prover endpoint.

You can use BigInt or Base64 encoding for extendedEphemeralPublicKey, jwtRandomness, and salt:

curl -X POST $PROVER_URL -H 'Content-Type: application/json' \
-d '{
"jwt": "$JWT_TOKEN",
"extendedEphemeralPublicKey": "84029355920633174015103288781128426107680789454168570548782290541079926444544",
"maxEpoch": "10",
"jwtRandomness": "100681567828351849884072155819400689117",
"salt": "248191903847969014646285995941615069143",
"keyClaimName": "sub"
}'

The response contains the proof components:

{
"proofPoints": {
"a": ["17267520...", "14650660...", "1"],
"b": [["21139310...", "10547097..."], ["12744153...", "17883388..."], ["1", "0"]],
"c": ["14769767...", "19108054...", "1"]
},
"issBase64Details": {
"value": "wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw",
"indexMod4": 2
},
"headerBase64": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ"
}

Handling CORS errors

To avoid CORS errors in frontend apps, proxy the prover call through your backend:

import { getZkLoginSignature } from '@mysten/sui/zklogin';

const proofResponse = await post('/your-internal-api/zkp/get', zkpRequestPayload);

export type PartialZkLoginSignature = Omit<
Parameters<typeof getZkLoginSignature>['0']['inputs'],
'addressSeed'
>;
const partialZkLoginSignature = proofResponse as PartialZkLoginSignature;

Option B: Self-hosted proving service

  1. Install Git Large File Storage before downloading the zkey.

  2. Download the Groth16 proving key zkey file:

    wget -O - https://raw.githubusercontent.com/sui-foundation/zklogin-ceremony-contributions/main/download-main-zkey.sh | bash

    Verify the hash: b2sum zkLogin-main.zkey

    Expected: 060beb961802568ac9ac7f14de0fbcd55e373e8f5ec7cc32189e26fb65700aa4e36f5604f868022c765e634d14ea1cd58bd4d79cef8f3cf9693510696bcbcbce

  3. Run the prover using Docker Compose. Create a docker-compose.yml:

    services:
    backend:
    image: mysten/zklogin:prover-stable
    volumes:
    - ${ZKEY}:/app/binaries/zkLogin.zkey
    environment:
    - ZKEY=/app/binaries/zkLogin.zkey
    - WITNESS_BINARIES=/app/binaries

    frontend:
    image: mysten/zklogin:prover-fe-stable
    command: '8080'
    ports:
    - '${PROVER_PORT}:8080'
    environment:
    - PROVER_URI=http://backend:8080/input
    - NODE_ENV=production
    - DEBUG=zkLogin:info,jwks
    # Default timeout is 15 seconds. Uncomment to change:
    # - PROVER_TIMEOUT=30
    ZKEY=<path_to_zkLogin.zkey> PROVER_PORT=<port> docker compose up
  4. The service exposes two endpoints:

    • /ping: health check (returns pong)
    • /v1: proof generation (same request/response format as the Mysten Labs service)
Prover resource requirements

The backend service (mysten/zklogin:prover-stable) is compute-heavy. Use at least 16 cores and 16 GB RAM. Weaker instances can cause timeout errors ("Call to rapidsnark service took longer than 15s"). Adjust with PROVER_TIMEOUT=30 for a 30-second timeout.

Set DEBUG=zkLogin:info,jwks in production. Using DEBUG=* turns on all logs, some of which contain PII.

If you need better performance, compile the prover from the rapidsnark fork and launch it in server mode.

Step 7: Assemble the zkLogin signature and submit

Sign the transaction with the ephemeral private key, then combine it with the zero-knowledge proof to create a zkLogin signature.

import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
import { SuiGrpcClient } from '@mysten/sui/grpc';
import { Transaction } from '@mysten/sui/transactions';
import { genAddressSeed, getZkLoginSignature } from '@mysten/sui/zklogin';

// 1. Sign the transaction with the ephemeral key
const client = new SuiGrpcClient({ baseUrl: '<YOUR_RPC_URL>', network: 'testnet' });
const txb = new Transaction();
txb.setSender(zkLoginUserAddress);

const { bytes, signature: userSignature } = await txb.sign({
client,
signer: ephemeralKeyPair, // must be the same key pair used for the zero-knowledge proof
});

// 2. Generate the address seed
const addressSeed = genAddressSeed(
BigInt(userSalt!),
'sub',
decodedJwt.sub,
decodedJwt.aud,
).toString();

// 3. Assemble the zkLogin signature
const zkLoginSignature = getZkLoginSignature({
inputs: {
...partialZkLoginSignature,
addressSeed,
},
maxEpoch,
userSignature,
});

// 4. Execute the transaction
const result = await client.executeTransaction({
transaction: bytes,
signatures: [zkLoginSignature],
});

Alternative: use ZkLoginSigner

Instead of manually assembling signatures in Step 7, you can use the ZkLoginSigner class to handle proof wrapping automatically. This is useful when you want to sign multiple transactions without repeating the signature assembly code.

import { ZkLoginSigner } from '@mysten/sui/zklogin';

const zkSigner = new ZkLoginSigner({
ephemeralSigner: ephemeralKeyPair,
maxEpoch,
inputs: {
...partialZkLoginSignature,
addressSeed,
},
legacyAddress: false,
client,
});

// Sign and execute — proof wrapping happens automatically
const txb = new Transaction();
txb.setSender(zkLoginUserAddress);
const { bytes, signature } = await zkSigner.signTransaction(await txb.build({ client }));
await client.executeTransaction({ transaction: bytes, signatures: [signature] });

See ZkLoginSigner for the full API reference.

Caching the ephemeral key and zero-knowledge proof

Each zero-knowledge proof is tied to an ephemeral key pair. You can reuse the proof to sign any number of transactions until the current epoch crosses maxEpoch.

Cache the ephemeral key pair and zero-knowledge proof for the duration of the session. However, treat them as secrets:

  • Use session storage, not local storage on browsers. Session storage clears automatically when the browser session ends.
  • Never persist them in an insecure location on any platform.
  • If both the ephemeral private key and zero-knowledge proof are compromised, an attacker can sign transactions on the user's behalf.

Efficiency considerations

zkLogin proofs take longer to generate than traditional signatures. The Mysten Labs prover typically returns a proof in about three seconds on a machine with 16 vCPUs and 64 GB RAM.

The right capacity metric is active user sessions, not individual signatures, because you cache and reuse each proof across a session. For example, one million daily active sessions with evenly distributed traffic requires roughly 1–2 requests per second (RPS) from the prover.

The Mysten Labs prover auto-scales for traffic surges. If you expect a spike in prover requests, reach out on Discord.

Troubleshooting

SymptomCauseFix
CORS error when calling proverFrontend calling prover directlyProxy prover calls through your backend
InvalidSignature from networkEphemeral key expired (epoch > maxEpoch)Generate a new key pair, re-authenticate, get a new zero-knowledge proof
InvalidSignature from networkNonce mismatch between JWT and signing keyEnsure you sign with the same ephemeral key pair used to compute the nonce
Prover timeout ("took longer than 15s")Prover machine is underpoweredUse at least 16 cores / 16 GB RAM, or increase PROVER_TIMEOUT
Salt service returns different addressaud or iss changedThe address depends on sub + iss + aud + salt. Any change produces a different address.
User locked out of addressSalt lostSalt loss is permanent. Ensure your salt service has backups and recovery procedures.