šŸ” Authentication Flow

SourceNet uses zkLogin - a privacy-preserving authentication system that combines Zero-Knowledge proofs with OAuth 2.0, allowing users to authenticate without exposing private keys.

What is zkLogin?

zkLogin is a cryptographic authentication method developed by Mysten Labs that enables users to:

  • Login with familiar OAuth providers (Google, Facebook, etc.)
  • No wallet installation required - reduces friction for new users
  • Maintain privacy - identity provider doesn't know blockchain address
  • Full control - users maintain complete control over their assets
  • Sign transactions - execute blockchain transactions securely

šŸ’” Key Concept

zkLogin derives a deterministic SUI address from the user's OAuth identity (e.g., Google ID) combined with a unique salt. This means the same Google account always maps to the same SUI address.

Complete Authentication Flow

Phase 1: OAuth Authentication

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  User   │ Clicks "Login with Google"
ā””ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”˜
     │
     ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  Frontend    │ Generate nonce & state
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
       │
       │ Redirect to Google OAuth
       ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   Google     │ Show consent screen
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
       │
       │ User approves
       ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  Frontend    │ Receive id_token (JWT)
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Step 1: Initiate OAuth Flow

// Frontend: Initiate Google OAuth
const initiateLogin = () => {
  const nonce = generateRandomNonce();
  const state = generateRandomState();
  
  // Store nonce for later verification
  sessionStorage.setItem('zklogin_nonce', nonce);
  
  const params = new URLSearchParams({
    client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
    redirect_uri: `${window.location.origin}/callback`,
    response_type: 'id_token',
    scope: 'openid email profile',
    nonce: nonce,
    state: state,
  });
  
  window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
};

Step 2: Handle OAuth Callback

// Frontend: Extract JWT from callback
const handleCallback = () => {
  const hash = window.location.hash;
  const params = new URLSearchParams(hash.substring(1));
  const idToken = params.get('id_token');
  
  // Decode JWT to extract user info
  const decoded = jwt_decode(idToken);
  console.log(decoded);
  // {
  //   sub: "105628...", // Google user ID
  //   email: "user@gmail.com",
  //   aud: "your-client-id",
  //   iss: "https://accounts.google.com"
  // }
};

Phase 2: zkLogin Setup

Step 3: Generate Ephemeral Key Pair

import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519';

// Generate ephemeral keypair (temporary, not stored)
const ephemeralKeyPair = new Ed25519Keypair();
const ephemeralPublicKey = ephemeralKeyPair.getPublicKey();

console.log('Ephemeral Public Key:', ephemeralPublicKey.toBase64());

Step 4: Request User Salt

// Frontend: Request salt from backend
const getSalt = async (sub: string) => {
  const response = await fetch('/api/auth/salt', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ sub })
  });
  
  const { salt } = await response.json();
  return salt; // e.g., "129390938109283091283091283091"
};
// Backend: Generate or retrieve salt
async function getUserSalt(sub: string): Promise<string> {
  let user = await User.findOne({ where: { google_id: sub } });
  
  if (!user) {
    // Generate new salt for new user
    const salt = generateRandomSalt(); // 256-bit random
    user = await User.create({
      google_id: sub,
      salt: salt
    });
  }
  
  return user.salt;
}

Step 5: Request ZK Proof from Mysten Labs

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

// Request ZK proof
const getZKProof = async (jwt: string, salt: string, ephemeralKeyPair) => {
  const response = await fetch('https://prover-dev.mystenlabs.com/v1', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      jwt,
      extendedEphemeralPublicKey: ephemeralKeyPair.getPublicKey().toSuiBytes(),
      maxEpoch: getCurrentEpoch() + 10,
      jwtRandomness: salt,
      salt: salt,
      keyClaimName: 'sub'
    })
  });
  
  const { zkProof, maxEpoch } = await response.json();
  return { zkProof, maxEpoch };
};

āš ļø Important

The ZK proof generation can take 5-10 seconds. Show a loading indicator to the user during this process.

Step 6: Derive SUI Address

import { jwtToAddress, genAddressSeed } from '@mysten/zklogin';

// Compute address seed
const addressSeed = genAddressSeed(
  BigInt(salt),
  'sub',
  jwtPayload.sub,
  jwtPayload.aud
);

// Derive SUI address
const suiAddress = jwtToAddress(jwt, salt);

console.log('SUI Address:', suiAddress);
// Output: "0x7b8a9c3d4e5f6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b"

Phase 3: Backend Authentication

Step 7: Authenticate with Backend

// Frontend: Send JWT and SUI address to backend
const authenticate = async (jwt: string, suiAddress: string) => {
  const response = await fetch('/api/auth/zklogin', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      id_token: jwt,
      sui_address: suiAddress
    })
  });
  
  const { token, user } = await response.json();
  
  // Store session token
  localStorage.setItem('token', token);
  
  return { token, user };
};

Step 8: Backend Validation

// Backend: Validate JWT and create session
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

async function zkloginAuth(req, res) {
  const { id_token, sui_address } = req.body;
  
  // 1. Verify JWT signature with Google's public key
  const client = jwksClient({
    jwksUri: 'https://www.googleapis.com/oauth2/v3/certs'
  });
  
  const getKey = (header, callback) => {
    client.getSigningKey(header.kid, (err, key) => {
      const signingKey = key.publicKey || key.rsaPublicKey;
      callback(null, signingKey);
    });
  };
  
  const decoded = await new Promise((resolve, reject) => {
    jwt.verify(id_token, getKey, {
      algorithms: ['RS256'],
      audience: process.env.GOOGLE_CLIENT_ID,
      issuer: 'https://accounts.google.com'
    }, (err, decoded) => {
      if (err) reject(err);
      else resolve(decoded);
    });
  });
  
  // 2. Find or create user
  const user = await User.findOrCreate({
    where: { google_id: decoded.sub },
    defaults: {
      email: decoded.email,
      name: decoded.name,
      sui_address: sui_address
    }
  });
  
  // 3. Generate session JWT
  const sessionToken = jwt.sign(
    {
      userId: user.id,
      suiAddress: user.sui_address,
      email: user.email
    },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );
  
  res.json({
    token: sessionToken,
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      suiAddress: user.sui_address
    }
  });
}

Transaction Signing with zkLogin

Once authenticated, users can sign blockchain transactions using their zkLogin credentials.

Building a Transaction

import { TransactionBlock } from '@mysten/sui.js/transactions';

// Create transaction block
const tx = new TransactionBlock();

// Example: Transfer SUI
const [coin] = tx.splitCoins(tx.gas, [tx.pure(1000000000)]); // 1 SUI
tx.transferObjects([coin], tx.pure(recipientAddress));

// Set sender
tx.setSender(userSuiAddress);

Signing with zkLogin

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

// Sign transaction with ephemeral key
const signature = await tx.sign({
  client: suiClient,
  signer: ephemeralKeyPair
});

// Create zkLogin signature
const zkLoginSignature = getZkLoginSignature({
  inputs: {
    ...zkProof,
    addressSeed: addressSeed.toString()
  },
  maxEpoch,
  userSignature: signature
});

// Execute transaction
const result = await suiClient.executeTransactionBlock({
  transactionBlock: tx,
  signature: zkLoginSignature,
  options: {
    showEffects: true,
    showObjectChanges: true
  }
});

console.log('Transaction digest:', result.digest);

Security Considerations

AspectSecurity Measure
Salt StorageSalts stored in database, never exposed to client
JWT ValidationSignature verified with Google's public keys
Ephemeral KeysGenerated per-session, never persisted
Session Expiry7-day JWT expiration, refresh required
Nonce ValidationPrevents replay attacks

Session Management

Storing Session

// Store in localStorage
localStorage.setItem('token', sessionToken);
localStorage.setItem('suiAddress', suiAddress);

// Store ephemeral keypair (encrypted)
const encryptedKey = encrypt(ephemeralKeyPair.export(), password);
sessionStorage.setItem('ephemeral_key', encryptedKey);

Refreshing Session

// Check token expiry
const isTokenExpired = () => {
  const token = localStorage.getItem('token');
  if (!token) return true;
  
  const decoded = jwt_decode(token);
  return decoded.exp < Date.now() / 1000;
};

// Re-authenticate if expired
if (isTokenExpired()) {
  // Redirect to login
  window.location.href = '/login';
}

āœ… Authentication Complete

Once authenticated, users can interact with the marketplace. Learn about the Buyer Flow to see how purchases work with zkLogin.