zkLogin Integration Guide
Here is the high-level flow the wallet or frontend application must implement to support zkLogin-enabled transactions:
- The wallet creates an ephemeral key pair.
- The wallet prompts the user to complete an OAuth login flow with the nonce corresponding to the ephemeral public key.
- After receiving the JSON Web Token (JWT), the wallet obtains a zero-knowledge proof.
- The wallet obtains a unique user salt based on a JWT. Use the OAuth subject identifier and salt to compute the zkLogin Sui address.
- The wallet signs transactions with the ephemeral private key.
- The wallet submits the transaction with the ephemeral signature and the zero-knowledge proof.
Let's dive into the specific implementation details.
Install the zkLogin TypeScript SDK
To use the zkLogin TypeScript SDK in your project, run the following command in your project root:
- npm
- Yarn
- pnpm
npm install @mysten/sui
yarn add @mysten/sui
pnpm add @mysten/sui
If you want to use the latest experimental version:
- npm
- Yarn
- pnpm
npm install @mysten/sui@experimental
yarn add @mysten/sui@experimental
pnpm add @mysten/sui@experimental
Get JWT
-
Generate an ephemeral key pair. Follow the same process as you would generating a key pair in a traditional wallet. See Sui SDK for details.
-
Set the expiration time for the ephemeral key pair. The wallet decides whether the maximum epoch is the current epoch or later. The wallet also determines whether this is adjustable by the user.
-
Assemble the OAuth URL with configured client ID, redirect URL, ephemeral public key and nonce: This is what the application sends the user to complete the login flow with a computed nonce.
import { generateNonce, generateRandomness } from '@mysten/sui/zklogin';
const FULLNODE_URL = 'https://fullnode.devnet.sui.io'; // replace with the RPC URL you want to use
const suiClient = new SuiClient({ url: FULLNODE_URL });
const { epoch, epochDurationMs, epochStartTimestampMs } = await suiClient.getLatestSuiSystemState();
const maxEpoch = Number(epoch) + 2; // this means the ephemeral key will be active for 2 epochs from now.
const ephemeralKeyPair = new Ed25519Keypair();
const randomness = generateRandomness();
const nonce = generateNonce(ephemeralKeyPair.getPublicKey(), maxEpoch, randomness);
The auth flow URL can be constructed with $CLIENT_ID
, $REDIRECT_URL
and $NONCE
.
For some providers ("Yes" for "Auth Flow Only"), the JWT can be found immediately in the redirect URL after the auth flow.
For other providers ("No" for "Auth Flow Only"), the auth flow only returns a code ($AUTH_CODE
) in redirect URL. To retrieve the JWT, an additional POST call is required with "Token Exchange URL".
Provider | Auth Flow URL | Token Exchange URL | Auth Flow Only |
---|---|---|---|
https://accounts.google.com/o/oauth2/v2/auth?client_id=$CLIENT_ID&response_type=id_token&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCE | N/A | Yes | |
https://www.facebook.com/v17.0/dialog/oauth?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&scope=openid&nonce=$NONCE&response_type=id_token | N/A | Yes | |
Twitch | https://id.twitch.tv/oauth2/authorize?client_id=$CLIENT_ID&force_verify=true&lang=en&login_type=login&redirect_uri=$REDIRECT_URL&response_type=id_token&scope=openid&nonce=$NONCE | N/A | Yes |
Kakao | https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCE | https://kauth.kakao.com/oauth/token?grant_type=authorization_code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&code=$AUTH_CODE | No |
Apple | https://appleid.apple.com/auth/authorize?client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&scope=email&response_mode=form_post&response_type=code%20id_token&nonce=$NONCE | N/A | Yes |
Slack | https://slack.com/openid/connect/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=$REDIRECT_URL&nonce=$NONCE&scope=openid | https://slack.com/api/openid.connect.token?code=$AUTH_CODE&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET | Yes |
Microsoft | https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=$CLIENT_ID&scope=openid&response_type=id_token&nonce=$NONCE&redirect_uri=$REDIRECT_URL | Yes |
Decoding JWT
Upon successful redirection, the OpenID provider attaches the JWT as a URL parameter. The following is an example using the Google flow.
http://host/auth?id_token=tokenPartA.tokenPartB.tokenPartC&authuser=0&prompt=none
The id_token
param is the JWT in encoded format. You can validate the correctness of the encoded token and investigate its structure by pasting it in the jwt.io website.
To decode the JWT you can use a library like: jwt_decode:
and map the response to the provided type JwtPayload
:
const decodedJwt = jwt_decode(encodedJWT) as JwtPayload;
export interface JwtPayload {
iss?: string;
sub?: string; //Subject ID
aud?: string[] | string;
exp?: number;
nbf?: number;
iat?: number;
jti?: string;
}
User salt management
zkLogin uses the user salt to compute the zkLogin Sui address (see definition). The salt must be a 16-byte value or a integer smaller than 2n**128n
. There are several options for the application to maintain the user salt:
- Client side:
- Option 1: Request user input for the salt during wallet access, transferring the responsibility to the user, who must then remember it.
- Option 2: Browser or Mobile Storage: Ensure proper workflows to prevent users from losing wallet access during device or browser changes. One approach is to email the salt during new wallet setup.
- Backend service that exposes an endpoint that returns a unique salt for each user consistently.
- Option 3: Store a mapping from user identifier (e.g.
sub
) to user salt in a conventional database (e.g.user
orpassword
table). The salt is unique per user. - Option 4: Implement a service that keeps a master seed value, and derive a user salt with key derivation by validating and parsing the JWT. For example, use
HKDF(ikm = seed, salt = iss || aud, info = sub)
defined here. Note that this option does not allow any rotation on master seed or change in client ID (i.e. aud), otherwise a different user address will be derived and will result in loss of funds.
- Option 3: Store a mapping from user identifier (e.g.
Here's an example request and response for the Mysten Labs-maintained salt server (using option 4). If you want to use the Mysten Labs salt server, please refer to Enoki docs and contact us. Only valid JWT authenticated with whitelisted 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"}
User salt is used to disconnect the OAuth identifier (sub) from the on-chain Sui address to avoid linking Web2 credentials with Web3 credentials. While losing or misusing the salt could enable this link, it wouldn't compromise fund control or zkLogin asset authority. See more discussion here.