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:

  1. 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.

  2. 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.

  3. 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.

Metamask/Coinbase Signup/Login Flow

The authentication process consists of two main API calls:

  1. GET /wallet/nonce

  2. 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.

ZCN Mnemonic Signup/Login Flow

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

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 existing uid.

  • 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 issued random_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 and expires_at match the stored challenge.

  • signed_message verifies against public_key for the exact bytes of random_message.

  • public_key is linked to a user (else return public_key_not_found).

Field reference

Field
Type
Description

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)

  1. FE → Obox: POST /firebase/custom-token { public_key, recaptcha_token }

  2. Obox → FE: { token, uid, phone_number?, email? }

  3. (Out of diagram scope) FE uses the token to start the session (see Token usage).

Login (returning)

  1. FE → Obox: GET /wallet/auth/challenge

  2. FE: Signs random_message locally with the mnemonic-derived key

  3. FE → Obox: POST /wallet/auth/verify { public_key, signed_message, random_message, expires_at, challenge_token }

  4. 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.

User Partner Wallet Login

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 to address, provider, and random_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 and provider match the challenge binding.

  • signed_message verifies for the exact random_message using wallet_address’s public key.

  • Partner wallet record exists and maps to wallet_id/uid.

End-to-end sequences

A) Happy path (wallet exists)

  1. FE → Obox: GET /wallet/partner/exists?address&provider

  2. Obox: returns { exists: true, wallet_id, uid }

  3. FE → Obox: GET /wallet/partner/challenge?address&provider

  4. FE: wallet signs random_message via personal_sign

  5. FE → Obox: POST /wallet/partner/verify { provider, wallet_address, signed_message, random_message, expires_at, challenge_token }

  6. Obox: returns { token, uid, wallet_id }

  7. FE: exchanges token with Firebase SDK to establish the session

B) Fallback (wallet not found)

  1. FE → Obox: GET /wallet/partner/exists?address&provider

  2. Obox: returns { exists: false }

  3. FE: fallback to “ZCN Mnemonic Signup/Login Flow” to create/link an account

  4. (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 active wallet_id used for the token.

  • Network guardrails: include chainId in the challenge and enforce it at verification time.

Last updated