Wallet-based Authentication Flows
Zus supports multiple wallet-based authentication methods to give users flexibility in how they access the platform. Instead of traditional username/password logins, authentication relies on cryptographic signatures that prove control of a wallet address.
The backend (“Obox”) then validates this proof and issues a Firebase Custom Token, which the frontend uses to establish a secure session.
There are three main flows, each serving a different user scenario:
Metamask / Coinbase Wallet Login Designed for users who prefer to sign in with widely used Web3 wallets. The flow issues a unique nonce, requires the user to sign it, and validates the signature before returning a session token.
ZCN Mnemonic Signup/Login Built around wallets derived from Zus Cloud Network mnemonics. The mnemonic never leaves the device; instead, the frontend proves wallet ownership by signing backend-issued challenges. This flow supports both first-time signup (with reCAPTCHA) and returning user login.
Partner Wallet Login Enables login through integrated partner wallets (e.g., MetaMask, Coinbase, Binance). If the wallet is recognized, the system verifies ownership and links it to an existing account. If not, the flow gracefully falls back to the ZCN mnemonic-based signup/login.
This section documents all three flows with their step-by-step sequences, API contracts, security considerations, and error models to help developers implement wallet-based authentication consistently across Zus applications.
1. Metamask/Coinbase Signup/Login Flow
This flow enables users to sign up or log in to the platform using their Metamask or Coinbase Wallet. It leverages wallet-based authentication and message signing to verify ownership of the wallet address before issuing a Firebase authentication token.
Overview
Frontend (FE): Initiates login requests and manages interaction with the wallet provider.
Obox: Backend service that issues nonces, verifies signed messages, and generates Firebase custom tokens.
Wallet (Metamask or Coinbase): Used by the user to sign messages cryptographically, proving ownership of the wallet address.

The authentication process consists of two main API calls:
GET /wallet/noncePOST /wallet/verify
Step-by-Step Flow
1. Request Nonce
Endpoint:
GET /wallet/nonceInitiated by: FE → Obox
Purpose: Generate a unique challenge (nonce + message) for the user to sign with their wallet.
Response Example:
{
"message": "Login request for authentication",
"nonce": "123456"
}2. Sign Message
The FE passes the received message and nonce to the connected wallet (Metamask or Coinbase).
The user signs the message with their wallet’s private key.
The wallet returns the signed payload back to the FE.
3. Verify Wallet Signature
Endpoint:
POST /wallet/verifyInitiated by: FE → Obox
Purpose: Send signed data to backend for verification.
Request Example:
{
"walletAddress": "0x1234abcd...",
"signature": "0xshjads9as...",
"provider": "coinbase",
"nonce": "123456"
}4. Backend Verification
Obox validates the request by checking:
The signature matches the wallet address.
The nonce is correct and not reused.
If successful, Obox generates a Firebase custom token for the user session.
Response Example:
{
"message": "certified successfully",
"data": "firebaseCustomTokenHere"
}Security Considerations
Nonce Uniqueness: Prevents replay attacks. Each nonce can only be used once.
Message Signing: Ensures that only the wallet owner can authenticate.
Firebase Custom Token: Used to manage authenticated sessions securely on the frontend
2. ZCN Mnemonic Signup/Login Flow
This section documents the wallet-based authentication shown in the “ZCN Mnemonic Signup/Login flow” diagram. Users authenticate by proving control of a wallet derived from a mnemonic. The mnemonic never leaves the device. The backend (“Obox”) issues short-lived challenges and, on successful verification, returns a firebase_custom_token and uid. (Firebase itself isn’t shown in the diagram; see Token usage below.)
Actors
FE (Frontend) — Derives the wallet from the user’s mnemonic locally, requests challenges, signs them, and calls the auth APIs.
Obox (Backend) — Creates challenges, verifies signatures, links public keys to users, and returns
firebase_custom_token.

When to use each flow
First time on a device / new user: Sign-up / link key via
POST /firebase/custom-token.Returning user: Login via
GET /wallet/auth/challenge→ sign →POST /wallet/auth/verify.
API contracts
1) Sign-up / Link Public Key
POST /firebase/custom-token
Creates (or fetches) a user linked to the provided public_key. Requires a valid reCAPTCHA token.
Request
{
"public_key": "04a162a3d4e5f...",
"recaptcha_token": "recaptcha_token"
}Response 200
{
"token": "firebase_custom_token",
"uid": "firebase_user_uid",
"phone_number": "user_phone_number",
"email": "user_email"
}Notes
Idempotent: if the
public_keyis already linked, returns a fresh custom token and existinguid.recaptcha_tokenis validated server-side.
2) Start Login (issue challenge)
GET /wallet/auth/challenge
Issues a single-use challenge the FE must sign exactly as provided.
Response 200
{
"random_message": "440004d8f1",
"expires_at": 1754163986,
"challenge_token": "69a5fd2566b46e"
}Notes
expires_atis UNIX time (seconds).challenge_tokenis single-use and bound to the issuedrandom_message.
3) Complete Login (verify signature)
POST /wallet/auth/verify
Verifies that the signature corresponds to public_key and the issued random_message and challenge_token.
Request
{
"public_key": "04a162a3d4e5f...",
"signed_message": "3045022100a1890a6b...",
"random_message": "440004d8f1",
"expires_at": 1754163986,
"challenge_token": "69a5fd2566b46e"
}Response 200
{
"token": "firebase_custom_token",
"uid": "firebase_uid"
}Server checks
challenge_tokenexists, unused, and not expired.random_messageandexpires_atmatch the stored challenge.signed_messageverifies againstpublic_keyfor the exact bytes ofrandom_message.public_keyis linked to a user (else returnpublic_key_not_found).
Field reference
public_key
string
Hex/Base58 (implementation-defined). Must match FE/BE convention (compressed vs uncompressed).
recaptcha_token
string
reCAPTCHA token collected on FE during sign-up.
token
string
Firebase Custom Token to establish the FE session.
uid
string
User id associated with public_key.
phone_number, email
string?
Optional profile fields if stored for the user.
random_message
string
Opaque challenge payload to sign exactly as given.
expires_at
number
UNIX seconds until the challenge expires.
challenge_token
string
One-time handle for the issued challenge.
signed_message
string
Signature over random_message using the wallet private key (encoding per implementation).
End-to-end sequences
Sign-up (first time / linking)
FE → Obox:
POST /firebase/custom-token { public_key, recaptcha_token }Obox → FE:
{ token, uid, phone_number?, email? }(Out of diagram scope) FE uses the token to start the session (see Token usage).
Login (returning)
FE → Obox:
GET /wallet/auth/challengeFE: Signs
random_messagelocally with the mnemonic-derived keyFE → Obox:
POST /wallet/auth/verify { public_key, signed_message, random_message, expires_at, challenge_token }Obox → FE:
{ token, uid }
Security & correctness
Mnemonic privacy: Mnemonic and private key never leave the FE.
Replay protection:
challenge_tokenis single-use; mark as consumed after success.Expiry enforcement: Reject any challenge past
expires_at(allow small clock skew).Exact-bytes signing: FE must sign the exact
random_message(no trimming, prefixes, or encoding changes).Rate limiting: Apply per IP and per
public_keyfor challenge and verify.Audit logs: Record sign-ups, challenge issuance, and verification attempts with non-PII identifiers.
Algorithm agreement: FE and Obox must agree on curve, signature format (e.g., DER hex vs raw 64-byte base64), and public key format.
3. User Partner Wallet Login
This flow lets a user log in with a partner wallet (e.g., MetaMask, Coinbase Wallet, Binance Web3). If the partner wallet isn’t recognized in our system, we fallback to the ZCN Mnemonic Signup/Login flow.
Purpose
Support passwordless login with external wallets
Verify the user by a signed challenge
Map the partner wallet to an existing account and return a
firebase_custom_token
Actors
FE (Frontend): Connects to the chosen partner wallet, requests challenges, signs them, calls APIs.
Obox (Backend): Checks existence, issues/verifies challenges, validates provider mapping, returns
firebase_custom_token.

Prerequisites
FE can request the active wallet address from the chosen provider.
Both sides agree on:
Chain/curve: (e.g., Ethereum
secp256k1)Signing method: EIP-191
personal_sign(or provider-specific equivalent)Key/address format: checksum-normalized hex (all addresses should be normalized server-side)
Data model (server)
Table: partner_wallets
id(pk)provider(metamask|coinbase|binance|...)wallet_address(checksum-normalized, unique per provider)user_id(maps to account /uid)wallet_id(internal wallet identifier, if applicable)created_at,last_login_at,status
Endpoints
1) Check existence
GET /wallet/partner/exists?address={addr}&provider={provider}
Response 200
{ "exists": true, "wallet_id": "w_123", "uid": "firebase_uid" }or
{ "exists": false }Notes
Normalize
addressbefore lookup.If
exists: false, FE should fallback to ZCN Mnemonic Signup/Login (see that section).
2) Issue challenge (only if exists)
GET /wallet/partner/challenge?address={addr}&provider={provider}
Response 200
{
"random_message": "8a4d0da9c0",
"expires_at": 1754163986,
"challenge_token": "c9d1f0f4a1"
}Notes
Single-use
challenge_token, bound toaddress,provider, andrandom_message.expires_atin UNIX seconds (allow small clock skew).
3) Verify signature (complete login)
POST /wallet/partner/verify
Request
{
"provider": "coinbase",
"wallet_address": "0x1234...ABCD",
"signed_message": "0x3045022100...",
"random_message": "8a4d0da9c0",
"expires_at": 1754163986,
"challenge_token": "c9d1f0f4a1"
}Response 200
{
"message": "certified successfully",
"token": "firebase_custom_token",
"uid": "firebase_uid",
"wallet_id": "w_123"
}Server checks
challenge_tokenexists, unused, not expired.random_message&expires_atmatch the stored challenge.wallet_addressandprovidermatch the challenge binding.signed_messageverifies for the exactrandom_messageusingwallet_address’s public key.Partner wallet record exists and maps to
wallet_id/uid.
End-to-end sequences
A) Happy path (wallet exists)
FE → Obox:
GET /wallet/partner/exists?address&providerObox: returns
{ exists: true, wallet_id, uid }FE → Obox:
GET /wallet/partner/challenge?address&providerFE: wallet signs
random_messageviapersonal_signFE → Obox:
POST /wallet/partner/verify { provider, wallet_address, signed_message, random_message, expires_at, challenge_token }Obox: returns
{ token, uid, wallet_id }FE: exchanges
tokenwith Firebase SDK to establish the session
B) Fallback (wallet not found)
FE → Obox:
GET /wallet/partner/exists?address&providerObox: returns
{ exists: false }FE: fallback to “ZCN Mnemonic Signup/Login Flow” to create/link an account
(Optional) Offer a post-signup step in profile settings to map the partner wallet
FE notes
Use the provider’s recommended signing method (e.g.,
ethereum.request({ method: "personal_sign", params: [hexMessage, address] })for EVM wallets).Sign exactly the
random_messageas returned; don’t add banners/prefixes unless the backend expects them.On
{ exists: false }, route to ZCN Mnemonic Signup/Login UI.
Optional extensions
Link partner wallet in user settings (after mnemonic signup) by running the challenge/verify sequence and persisting
(provider, address) → {uid, wallet_id}.Multi-wallet accounts: allow multiple partner wallets mapped to the same
uid; return the activewallet_idused for the token.Network guardrails: include
chainIdin the challenge and enforce it at verification time.
Last updated