Skip to content

Authentication

Partly out of date

Rendered from ProjectInfo/client/authentication.md on docs/encryption-modes-spec. Its protocol content is still useful, but parts of it describe an older client or an earlier API. Where it disagrees with the code, the code wins — see the spec errata.

Registration

POST API_BASE/api/auth/register
Content-Type: application/json

{
  "username": "alice",
  "email": "[email protected]",
  "password": "hunter2!!"    // minimum 8 characters
}

Response 201:

{
  "access_token": "eyJ...",
  "refresh_token": "<raw>",
  "user": { "id": "uuid", "username": "alice", "email": "[email protected]" }
}

Errors: 400 validation, 409 email already used

After registration, immediately run the E2EE key setup flow.


Login

POST API_BASE/api/auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "hunter2!!"
}

Response 200: same shape as register
Errors: 401 invalid credentials

After login, run the post-auth key checks and check OTP count.


Token Refresh

Access tokens expire in 15 minutes. Refresh before expiry — do not wait for a 401.

POST API_BASE/api/auth/refresh
Content-Type: application/json

{ "refresh_token": "<raw-refresh-token>" }

Response 200: new access_token + new refresh_token
Errors: 401 — token invalid, expired, or already used

Critical behaviour: - Refresh tokens are single-use — the old token is revoked on use - Store the new refresh_token immediately before doing anything else - If a refresh returns 401, redirect to login — the session is gone - Fire the refresh at 13 minutes after the last access_token was issued (not at expiry)

Recommend a background timer or interceptor that tracks the token issue time from the JWT iat claim.


JWT Claims

{
  "uid": "user-uuid",
  "did": "device-id-string",
  "bot": false,
  "iss": "zipp-chat",
  "iat": 1700000000,
  "exp": 1700000900
}

The did (device ID) is required for all E2EE operations. Parse it from the JWT immediately after login/register and store it alongside your key material.


Token Storage

Token Where Notes
access_token In-memory only Short-lived; never persist to disk
refresh_token Secure local storage 7-day lifetime; treat like a password

Recommended storage per platform: - Desktop (Tauri/Electron): OS credential store (keychain/libsecret) - Mobile: iOS Keychain / Android Keystore - Web: localStorage under key zipp_refresh_token (acceptable for web; prefer httpOnly cookies if server-rendered)


Post-Auth / Key Setup

Run this sequence after every login or registration. It is idempotent.

First time (no local key material)

1. Generate Ed25519 identity keypair (IK)
   → store private key securely; upload public key

2. Generate X25519 signed prekey (SPK):
   a. Generate X25519 keypair
   b. Sign SPK public key with IK private key (Ed25519 detached sig)
   c. Assign SPK ID = 1 (increment on each future rotation)

3. Generate 100 X25519 one-time prekeys (OTPs):
   - Unique monotonically-increasing IDs
   - Store all private halves locally

4. POST API_BASE/api/keys/bundle
   {
     "identity_key": base64(IK_pub),
     "signed_prekey": base64(SPK_pub),
     "signed_prekey_id": 1,
     "signed_prekey_signature": base64(sig)
   }

5. POST API_BASE/api/keys/prekeys
   {
     "prekeys": [
       { "key_id": 1, "public_key": base64(otp_pub) },
       ...  // 100 entries
     ]
   }

On every login (keys already exist)

1. Parse `did` from JWT → confirm it matches stored device ID
   If mismatch → this is a new device; run first-time setup instead

2. GET API_BASE/api/keys/prekeys/count
   → if count < 20: generate 80 new OTPs + POST /api/keys/prekeys

3. Check SPK age:
   → if signed prekey is > 7 days old:
     a. Generate new X25519 keypair
     b. Sign with IK, increment SPK ID
     c. POST /api/keys/bundle (updates server record)
     d. Retain old SPK locally until sessions using it ratchet forward

Fetching the Full Profile

The login/register response only returns { id, username, email }. Fetch the full profile for display:

GET API_BASE/api/users/@me
Authorization: Bearer <access_token>

Returns the full User object including display_name, avatar_url, status, etc.


Logout

Client-side only:

  1. Clear the refresh_token from secure storage
  2. Clear the access_token from memory
  3. Clear all locally cached channel keys and E2EE sessions if desired
  4. Navigate to login screen

The server automatically expires the refresh token after 7 days. There is no server-side logout endpoint.