Back to Merlin|Developer Docs

Authentication Flow

Overview

Merlin uses WebAuthn passkeys for passwordless authentication. No email, no social login — just biometrics (Face ID, fingerprint, Windows Hello). The passkey protects an encrypted BIP-39 seed phrase stored locally.

Registration Flow

1. User taps "Create Account"
2. Frontend: POST /api/v1/auth/register/begin {display_name}
3. Backend: generates WebAuthn registration options (RP ID, challenge, user ID)
4. Backend: stores challenge in Firestore (5-min TTL, one-time use)
5. Frontend: navigator.credentials.create() — browser shows biometric prompt
6. User authenticates with biometrics
7. Frontend: POST /api/v1/auth/register/complete {credential attestation}
8. Backend: py-webauthn verifies attestation
9. Backend: creates user in Firestore, stores credential (public key, sign count)
10. Backend: returns JWT (24h expiry) + user_id
11. Frontend: generates BIP-39 mnemonic (24 words) via @scure/bip39
12. Frontend: derives encryption key from credential ID via HKDF-SHA256
13. Frontend: encrypts seed with Scrypt (N=131072, r=8, p=1) + AES-128-CTR + keccak256 MAC
14. Frontend: stores encrypted blob in IndexedDB
15. Frontend: derives ETH address from seed (BIP-44: m/44'/60'/0'/0/0) via @scure/bip32
16. Frontend: PATCH /api/v1/auth/address {address} — stores derived address

Login Flow

1. User taps "Login"
2. Frontend: POST /api/v1/auth/login/begin {}
3. Backend: generates authentication options (discoverable credentials)
4. Backend: stores challenge in Firestore (5-min TTL)
5. Frontend: navigator.credentials.get() — browser shows biometric prompt
6. User authenticates
7. Frontend: POST /api/v1/auth/login/complete {credential assertion}
8. Backend: py-webauthn verifies assertion, updates sign count
9. Backend: returns JWT (24h expiry) + user info
10. Frontend: derives encryption key from credential ID via HKDF-SHA256
11. Frontend: decrypts seed from IndexedDB
12. Frontend: WalletManager unlocked — wallet ready

Session Management

  • JWT tokens: 24h expiry, stateless (no server-side sessions)
  • WalletManager: in-memory decrypted seed with 15-min auto-lock
  • Auto-lock: timer resets on activity, wallet re-locks requiring re-authentication
  • Re-auth: sensitive operations (export seed, execute trade) require unlocked wallet

Seed Import/Export

  • Import: validate BIP-39 mnemonic → encrypt with current passkey-derived key → store in IndexedDB → re-derive address
  • Export: decrypt seed from IndexedDB using in-memory key → display to user (sensitive)

Security Model

  • Private keys never leave the browser
  • Backend stores only public key material (WebAuthn credential public key)
  • Seed encrypted at rest with passkey-derived secret
  • Challenge store: one-time use, 5-min TTL, Firestore-backed
  • No session cookies — JWT in Authorization header

Key Files

File Purpose
frontend/lib/auth.ts AuthContext, login/signup/logout, seed import/export
frontend/components/providers/auth-provider.tsx AuthProvider implementation
frontend/components/auth-gate.tsx Blocks UI until authenticated
frontend/components/auth-guard.tsx Route protection
backend/auth/webauthn.py py-webauthn ceremonies
backend/auth/session.py JWT creation/verification
backend/auth/models.py Pydantic models
backend/auth/dependencies.py get_current_user dependency
backend/routers/auth.py 6 auth endpoints
backend/db/users.py User CRUD
backend/db/challenges.py Challenge storage