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/nonce
POST /wallet/verify
Step-by-Step Flow
1. Request Nonce
Endpoint:
GET /wallet/nonce
Initiated 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/verify
Initiated 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_key
is already linked, returns a fresh custom token and existinguid
.recaptcha_token
is 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_at
is UNIX time (seconds).challenge_token
is 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_token
exists, unused, and not expired.random_message
andexpires_at
match the stored challenge.signed_message
verifies againstpublic_key
for the exact bytes ofrandom_message
.public_key
is 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/challenge
FE: Signs
random_message
locally 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_token
is 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_key
for 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
address
before 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_at
in 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_token
exists, unused, not expired.random_message
&expires_at
match the stored challenge.wallet_address
andprovider
match the challenge binding.signed_message
verifies for the exactrandom_message
usingwallet_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&provider
Obox: returns
{ exists: true, wallet_id, uid }
FE → Obox:
GET /wallet/partner/challenge?address&provider
FE: wallet signs
random_message
viapersonal_sign
FE → Obox:
POST /wallet/partner/verify { provider, wallet_address, signed_message, random_message, expires_at, challenge_token }
Obox: returns
{ token, uid, wallet_id }
FE: exchanges
token
with Firebase SDK to establish the session
B) Fallback (wallet not found)
FE → Obox:
GET /wallet/partner/exists?address&provider
Obox: 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_message
as 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_id
used for the token.Network guardrails: include
chainId
in the challenge and enforce it at verification time.
Last updated