Skip to main content

Identity & Auth

Identity blends wallet‑native control with zkLogin single sign‑on and sponsored transactions to remove onboarding friction while preserving cryptographic assurances. This section explains how principals are established, how signatures are produced and verified, and how sponsorship safely covers gas without creating a trusted login server.

Principals and accounts

  • Wallet accounts: standard Sui addresses managed by user wallets; transactions are signed with the user’s key material.
  • zkLogin principals: identities derived from OpenID Connect (OIDC) IdPs, transformed into Sui addresses via zk proofs in the zkLogin flow. The result is a Sui address with verifiable provenance to the IdP without revealing credentials to the chain.

zkLogin in practice

zkLogin allows users to authenticate with familiar IdPs (Google, Apple, Facebook, Twitch, Slack) while preserving privacy and non‑custodial control. The system uses ephemeral keys for session management and PKCE flow for enhanced security. A high‑level flow (see Sui's zkLogin docs for precise primitives):

Key properties

  • The IdP never sees on‑chain activity; the chain never sees raw IdP tokens.
  • The zk proof binds IdP token claims (issuer, audience, expiry) to a Sui address under user control; tokens can expire without breaking the address.
  • Ephemeral keys are generated per session and stored client-side with configurable expiry (typically 24 hours to 30 days).
  • Salt generation follows Sui's recommendations for privacy and multi-device support.
  • Users can later migrate to a wallet account; both modes can coexist.

Wallets and signature prompts

  • Wallet integrations follow standard Sui wallet adapters. Sensitive operations (publishing, resolution, payouts) always require explicit user signatures.
  • Keys remain client‑side; lost devices require re‑establishing control (wallet recovery or repeating zkLogin binding).

Sponsored transactions remove the need for users to hold SUI to complete core actions. The pattern uses co‑signing and network validation (see Sui docs on sponsored transactions). This enables web2-like UX where readers can access content immediately without acquiring cryptocurrency first.

Sponsorship Implementation

module pml::sponsorship {
use sui::clock::{Self, Clock};
use sui::balance::{Self, Balance};
use sui::sui::SUI;

/// Gas sponsor pool
struct SponsorPool has key {
id: UID,
balance: Balance<SUI>,
daily_limit: u64,
per_user_limit: u64,
used_today: Table<address, u64>,
last_reset: u64
}

/// Sponsorship request (hot potato)
struct SponsorshipRequest {
requester: address,
action_type: u8, // 0: purchase, 1: subscription, 2: market
amount_required: u64
}

/// Check sponsorship eligibility
public fun check_eligibility(
pool: &mut SponsorPool,
requester: address,
amount: u64,
clock: &Clock
): bool {
// Reset daily limits if new day
let today = clock::timestamp_ms(clock) / 86400000;
if (pool.last_reset < today) {
table::drop(&mut pool.used_today);
pool.used_today = table::new(ctx);
pool.last_reset = today;
}

// Check user's daily usage
let user_used = if (table::contains(&pool.used_today, requester)) {
*table::borrow(&pool.used_today, requester)
} else {
0
};

// Verify limits
user_used + amount <= pool.per_user_limit &&
balance::value(&pool.balance) >= amount
}

/// Process sponsorship
public fun sponsor_transaction(
pool: &mut SponsorPool,
request: SponsorshipRequest,
clock: &Clock,
ctx: &mut TxContext
): Coin<SUI> {
let SponsorshipRequest { requester, action_type, amount_required } = request;

// Verify eligibility
assert!(
check_eligibility(pool, requester, amount_required, clock),
E_NOT_ELIGIBLE
);

// Update usage
if (table::contains(&pool.used_today, requester)) {
let used = table::borrow_mut(&mut pool.used_today, requester);
*used = *used + amount_required;
} else {
table::add(&mut pool.used_today, requester, amount_required);
};

// Withdraw gas amount
coin::from_balance(balance::split(&mut pool.balance, amount_required), ctx)
}
}

Abuse controls

  • Rate limits and per‑principal quotas; deny‑list abuse sources.
  • Require minimal proof‑of‑personness (e.g., zkLogin freshness) for sponsorship eligibility.
  • Cap maximum sponsored gas per operation and per day.

Roles and permissions

Capability-Based Access Control

module pml::capabilities {
/// Reader capability - basic access
struct ReaderCap has key {
id: UID,
zklogin_address: Option<address>,
sponsored_quota: u64,
created_at: u64
}

/// Journalist capability - publishing rights
struct JournalistCap has key {
id: UID,
verified: bool,
publications_count: u64,
earnings_total: u64
}

/// Source capability - anonymous submission
struct SourceCap has key {
id: UID,
reputation_id: ID,
submissions_count: u64,
trust_level: u8
}

/// Moderator capability - content moderation
struct ModeratorCap has key {
id: UID,
jurisdiction: vector<u8>, // Category or region
actions_taken: u64
}

/// Admin capability - system management
struct AdminCap has key {
id: UID
}

/// Issue reader capability with zkLogin
public entry fun issue_reader_cap(
zklogin_address: address,
clock: &Clock,
ctx: &mut TxContext
) {
let cap = ReaderCap {
id: object::new(ctx),
zklogin_address: option::some(zklogin_address),
sponsored_quota: 10, // Daily sponsored transactions
created_at: clock::timestamp_ms(clock)
};

transfer::transfer(cap, tx_context::sender(ctx));
}

/// Upgrade reader to journalist
public entry fun upgrade_to_journalist(
reader_cap: ReaderCap,
verification_proof: vector<u8>, // KYC or other verification
ctx: &mut TxContext
) {
// Verify proof (simplified)
assert!(vector::length(&verification_proof) > 0, E_INVALID_PROOF);

// Burn reader cap
let ReaderCap { id, .. } = reader_cap;
object::delete(id);

// Issue journalist cap
let journalist_cap = JournalistCap {
id: object::new(ctx),
verified: true,
publications_count: 0,
earnings_total: 0
};

transfer::transfer(journalist_cap, tx_context::sender(ctx));
}
}
  • Reader: view public content; purchase entitlements; participate in markets as allowed.
  • Journalist: publish, configure licensing and royalties, open/settle bounties.
  • Source: submit encrypted materials; receive payouts.
  • Moderator/Admin: apply moderation outcomes; manage catalogs; no superuser access to private media.

Privacy and sessions

Session Management with zkLogin

module pml::sessions {
use sui::clock::{Self, Clock};

/// Ephemeral session for zkLogin users
struct Session has key {
id: UID,
zklogin_address: address,
ephemeral_public_key: vector<u8>,
max_epoch: u64,
expiry: u64,
nonce: vector<u8>
}

/// Create new zkLogin session
public entry fun create_session(
zklogin_address: address,
ephemeral_public_key: vector<u8>,
max_epoch: u64,
session_duration_days: u64,
nonce: vector<u8>,
clock: &Clock,
ctx: &mut TxContext
) {
let session = Session {
id: object::new(ctx),
zklogin_address,
ephemeral_public_key,
max_epoch,
expiry: clock::timestamp_ms(clock) + (session_duration_days * 86400 * 1000),
nonce
};

transfer::transfer(session, zklogin_address);

event::emit(SessionCreated {
session_id: object::id(&session),
zklogin_address,
expiry: session.expiry
});
}

/// Rotate ephemeral keys during active session
public entry fun rotate_keys(
session: &mut Session,
new_ephemeral_public_key: vector<u8>,
new_max_epoch: u64,
clock: &Clock,
ctx: &mut TxContext
) {
assert!(tx_context::sender(ctx) == session.zklogin_address, E_UNAUTHORIZED);
assert!(clock::timestamp_ms(clock) < session.expiry, E_SESSION_EXPIRED);

session.ephemeral_public_key = new_ephemeral_public_key;
session.max_epoch = new_max_epoch;

// Extend session on rotation
session.expiry = clock::timestamp_ms(clock) + (30 * 86400 * 1000); // 30 days

event::emit(KeysRotated {
session_id: object::id(session),
new_expiry: session.expiry
});
}
}
  • Sessions are local: device‑resident salts and ephemeral keys for zkLogin with configurable expiry.
  • Ephemeral key rotation happens transparently during active sessions to maintain security.
  • Multi-device support through deterministic salt derivation from user credentials.
  • Clearing storage or moving devices requires re‑auth; no server‑side session invalidation is needed.
  • No centralized profile store is required to authorize reads; on‑chain checks always drive access.