Authentication
Zentalk authenticates users with an Ethereum wallet signature or a verified phone number, then manages the session with short-lived JWTs. This page covers every auth surface in the API.
Security schemes
Section titled “Security schemes”The OpenAPI specification defines three security schemes:
| Scheme | Header | Purpose |
|---|---|---|
BearerAuth |
Authorization: Bearer <jwt> |
Primary scheme. Short-lived JWT access token identifying the session. |
SignatureAuth |
X-Signature |
Ethereum wallet signature for request-level authentication on signature-gated operations. |
WalletAuth |
X-Wallet-Address |
Wallet address header. Informational only: since the IDOR remediation, the server derives identity from the JWT subject; this header is accepted only when it equals the JWT subject and is otherwise dropped. Never rely on it to select an identity — the JWT is authoritative. |
Path versioning
Section titled “Path versioning”Every route is registered under both /api/v1/ (current, recommended) and the unversioned /api/ prefix (legacy, deprecated); the endpoint tables below use the /api/v1/ paths. See Versioning.
Wallet login
Section titled “Wallet login”There is no server-issued challenge for wallet login. The client constructs the challenge itself, signs it, and submits it:
- Build a login message containing a
Timestamp: <unix-ms>line. - Sign it with the wallet (
personal_sign, EIP-191 — the server hashes the message with the\x19Ethereum Signed Message:\nprefix and recovers the signer address). POST /api/v1/initializewith the address, message, and signature.
curl -X POST http://localhost:3001/api/v1/initialize \ -H "Content-Type: application/json" \ -d '{ "wallet_address": "0x1234...5678", "username": "alice", "message": "Login to Zentalk\nTimestamp: 1768387200000", "signature": "0xabcdef..." }'Server-side protections on this endpoint:
| Protection | Behavior |
|---|---|
| Timestamp freshness | A Timestamp: line older than 5 minutes is rejected (EXPIRED_SIGNATURE). |
| Replay cache | Each signature is single-use for 10 minutes (REPLAY_DETECTED). |
| Recovered address check | The address recovered from the signature must match wallet_address. |
| Rate limiting + CAPTCHA | The endpoint is auth-rate-limited; a CAPTCHA gate wraps it when enabled by the operator. |
A successful response includes access_token, refresh_token, expires_at, and token_type: "Bearer". Calling /api/v1/initialize without signature/message acts as a status probe: it returns 401 (existing user — signature required) or 400 (new user — username required).
An existing session can also be resumed with a valid JWT instead of a fresh signature via POST /session/resume (BearerAuth).
Phone authentication
Section titled “Phone authentication”Phone auth is an alternative registration and login path. All endpoints are under the Authentication tag in the REST API reference.
| Step | Endpoint | Notes |
|---|---|---|
| 1. Request SMS code | POST /api/v1/auth/phone/send-sms |
CAPTCHA-gated; rate-limited per phone number and per IP. |
| 2. Verify code | POST /api/v1/auth/phone/verify-code |
Verifies the code sent in step 1. |
| 3a. Register | POST /api/v1/auth/phone/register |
Creates an account bound to the verified phone number. Returns 409 if already registered. |
| 3b. Login challenge | POST /api/v1/auth/phone/login-challenge |
Requests a challenge for phone-based login. |
| 4. Login | POST /api/v1/auth/phone/login |
Completes login using the verified challenge response. |
| Lockout status | GET /api/v1/auth/phone/lockout-status?phone=... |
Returns failed-attempt count and lock state for a number. |
Supporting endpoints: POST /api/v1/auth/migrate-phone-hash (upgrades a legacy V1 phone hash, PBKDF2-100k, to the V2 scheme, PBKDF2-310k; BearerAuth) and POST /api/v1/auth/phone-revoke-abandoned (revokes a partially completed registration attempt).
Phone numbers are never stored raw:
- Hashing — the client derives a PBKDF2-SHA256 hash on the device (310,000 iterations in the V2 scheme, 100,000 in legacy V1) using a fixed, public salt.
- Enumeration caveat — the hash is deliberately deterministic (contact discovery requires the same number to always produce the same hash), which limits resistance to offline enumeration of the phone-number space; the iteration count raises the cost of such enumeration but does not eliminate it.
- Registration binding — the client sends the phone hash signed with the user’s identity key; the backend currently runs this check in warn mode and accepts registrations without it.
- Login — the server verifies the challenge signature against the identity key.
JWT lifecycle
Section titled “JWT lifecycle”| Token | Lifetime | Notes |
|---|---|---|
| Access token | 15 minutes | Sent as Authorization: Bearer <token> on every API call. |
| Refresh token | 7 days | Exchanged for new token pairs; never sent as a Bearer token. |
curl -X POST http://localhost:3001/api/v1/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refresh_token": "eyJhbGciOi..."}'| Operation | Endpoint | Auth / notes |
|---|---|---|
| Refresh access token | POST /api/v1/auth/refresh |
Body: {"refresh_token": "..."} |
| Revoke a refresh token | POST /api/v1/auth/revoke |
BearerAuth + body {"refresh_token": "..."} |
Refresh tokens are organized in families with parent-chain tracking: presenting an already-used refresh token revokes the entire family. Optionally, refresh tokens can be DPoP-bound to a client-held key (RFC 9449); this is opt-in via NEXT_PUBLIC_DPOP_ENABLED=true in the web client.
Sessions and devices
Section titled “Sessions and devices”Sessions have a fixed 7-day lifetime from creation; an idle timeout exists but is disabled by default. A user can hold at most 5 concurrent device sessions.
| Operation | Endpoint | Auth / notes |
|---|---|---|
| List active sessions | GET /api/v1/auth/sessions |
BearerAuth |
| Revoke one session | DELETE /api/v1/auth/sessions/{id} |
BearerAuth |
| Revoke all sessions | POST /api/v1/auth/logout-all |
BearerAuth |
| Token logout (current session) | POST /api/v1/logout |
BearerAuth |
| Cookie session login | POST /api/v1/auth/session |
Starts a SameSite, HttpOnly cookie session (browser clients). |
| Cookie session logout | POST /api/v1/auth/session/logout |
BearerAuth; clears cookies and blacklists the current access token. |
| Current session info | GET /api/v1/session/me |
Cookie-based session check. |
Per-device refresh scoping adds a device registry:
| Operation | Endpoint |
|---|---|
| Register a device | POST /api/v1/auth/register-device |
| List devices | GET /api/v1/auth/devices |
| Revoke a device | DELETE /api/v1/auth/devices/{id} |
Browser clients using cookie sessions need a CSRF token. Rotate it with:
curl -X POST http://localhost:3001/api/v1/auth/csrf \ -H "Authorization: Bearer $ZENTALK_TOKEN"WebSocket tickets
Section titled “WebSocket tickets”Short-lived tickets are the recommended way to authenticate WebSocket connections; a JWT subprotocol fallback also exists. Request a ticket first:
curl -X POST http://localhost:3001/api/v1/auth/ws-ticket \ -H "Authorization: Bearer $ZENTALK_TOKEN"{ "ticket": "…", "expires_in": 30}The ticket is single-use and expires in 30 seconds — request it immediately before connecting to /ws. See WebSocket for the connection handshake and event format.
Multi-factor authentication
Section titled “Multi-factor authentication”MFA is TOTP-based with backup codes and trusted devices. All endpoints require BearerAuth.
| Operation | Endpoint | Notes |
|---|---|---|
| Begin TOTP setup | POST /api/v1/mfa/totp/setup |
Returns a provisioning URI and secret for the authenticator app. |
| Enable TOTP | POST /api/v1/mfa/totp/enable |
Confirms setup with a one-time code. |
| MFA status | GET /api/v1/mfa/status |
Whether MFA is enabled for the account. |
| Create login challenge | POST /api/v1/mfa/challenge |
Issued during login when MFA is enabled. |
| Verify challenge | POST /api/v1/mfa/verify |
Completes the MFA step. |
| Disable MFA | POST /api/v1/mfa/disable |
Requires verification. |
| Backup codes status | GET /api/v1/mfa/backup-codes/status |
Total, remaining, and used counts. |
| Regenerate backup codes | POST /api/v1/mfa/backup-codes/regenerate |
Requires TOTP verification; invalidates old codes. |
| Trusted devices | GET /api/v1/mfa/trusted-devices, DELETE /api/v1/mfa/trusted-devices/{deviceId} |
Manage devices that skip the MFA challenge. |
| Lockout status | GET /api/v1/mfa/lockout-status |
Failed-attempt lockout state. |
Rate limits
Section titled “Rate limits”Authentication attempts are limited to 5 per minute, and 429 responses carry Retry-After plus X-RateLimit-Limit and X-RateLimit-Window headers. See Rate limits for the full matrix.
Next steps
Section titled “Next steps”- Environments and base URLs — where to point your authenticated requests
- WebSocket connection — use your session for real-time events
- REST API reference — every authenticated endpoint
