From 0900289af52570dc64b33b2eeaa1bd44db633db6 Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 17 Mar 2026 02:17:59 -0700 Subject: [PATCH] docs: document register/login and dual authentication model Update README to accurately describe the authentication model: - Anonymous sessions via POST /api/v1/session (no account required) - Optional account registration via POST /api/v1/register (nick + password) - Login to registered accounts via POST /api/v1/login Sections updated: - Identity & Sessions: renamed from 'No Accounts' to 'Dual Authentication Model' - API Reference: added /register and /login endpoint documentation - Security Model: added password hashing (bcrypt) details - Design Principles: changed 'No accounts' to 'Accounts optional' - Schema: updated from outdated 'users' table to actual sessions/clients tables - Session Lifecycle: added registered account flow diagram - Client Development Guide: added register/login curl examples - Multi-Client Model: documented login as the multi-client mechanism - Data Lifecycle: documented session/client persistence behavior --- README.md | 269 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 232 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 2f48e23..fc18bcd 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,9 @@ mechanisms or stuffing data into CTCP. Everything else is IRC. `PRIVMSG`, `JOIN`, `PART`, `NICK`, `TOPIC`, `MODE`, `KICK`, `353`, `433` — same commands, same semantics. Channels start with `#`. Joining a nonexistent channel creates it. Channels disappear when empty. Nicks -are unique per server. There are no accounts — identity is a key, a nick is a -display name. +are unique per server. Identity starts with a key — a nick is a display name. +Accounts are optional: you can create an anonymous session instantly, or +register with a password to persist your identity across sessions. ### On the resemblance to JSON-RPC @@ -148,16 +149,43 @@ not arbitrary choices — each one follows from the project's core thesis that IRC's command model is correct and only the transport and session management need to change. -### Identity & Sessions — No Accounts +### Identity & Sessions — Dual Authentication Model -There are no accounts, no registration, no passwords. Identity is a signing -key; a nick is just a display name. The two are decoupled. +The server supports two authentication paths: **anonymous sessions** for +instant access, and **optional account registration** for persistent identity. + +#### Anonymous Sessions (No Account Required) + +The simplest entry point. No registration, no passwords. - **Session creation**: client sends `POST /api/v1/session` with a desired nick → server assigns an **auth token** (64 hex characters of cryptographically random bytes) and returns the user ID, nick, and token. - The auth token implicitly identifies the client. Clients present it via `Authorization: Bearer `. +- Anonymous sessions are ephemeral — when the session expires or the user + QUITs, the nick is released and there is no way to reclaim it. + +#### Registered Accounts (Optional) + +For users who want persistent identity across sessions: + +- **Registration**: client sends `POST /api/v1/register` with a nick and + password (minimum 8 characters) → server creates a session with the + password hashed via bcrypt, and returns the user ID, nick, and auth token. +- **Login**: client sends `POST /api/v1/login` with nick and password → + server verifies the password against the stored bcrypt hash and creates a + new client token for the existing session. This enables multi-client + access: logging in from a new device adds a client to the existing session + rather than creating a new one, so channel memberships and message queues + are shared. +- Registered accounts cannot be logged into via `POST /api/v1/session` — + that endpoint is for anonymous sessions only. +- Anonymous sessions (created via `/session`) cannot be logged into via + `/login` because they have no password set. + +#### Common Properties (Both Paths) + - Nicks are changeable via the `NICK` command; the server-assigned user ID is the stable identity. - Server-assigned IDs — clients do not choose their own IDs. @@ -165,11 +193,14 @@ key; a nick is just a display name. The two are decoupled. in the token, no client-side decode. The server is the sole authority on token validity. -**Rationale:** IRC has no accounts. You connect, pick a nick, and talk. Adding -registration, email verification, or OAuth would solve a problem nobody asked -about and add complexity that drives away casual users. Identity verification -is handled at the message layer via cryptographic signatures (see -[Security Model](#security-model)), not at the session layer. +**Rationale:** IRC has no accounts. You connect, pick a nick, and talk. +Anonymous sessions preserve that simplicity — instant access, zero friction. +But some users want to keep their nick across sessions without relying on a +bouncer or nick reservation system. Optional registration with password +solves this without adding friction for casual users: if you don't want an +account, don't create one. Identity verification at the message layer via +cryptographic signatures (see [Security Model](#security-model)) remains +independent of account registration. ### Nick Semantics @@ -207,12 +238,12 @@ User Session └── Client C (token_c, queue_c) ``` -**Current MVP note:** The current implementation creates a new user (with new -nick) per `POST /api/v1/session` call. True multi-client (multiple tokens -sharing one nick/session) is supported by the schema (`client_queues` is keyed -by user_id, and multiple tokens can point to the same user) but the session -creation endpoint does not yet support "add a client to an existing session." -This will be added post-MVP. +**Multi-client via login:** The `POST /api/v1/login` endpoint adds a new +client to an existing registered session, enabling true multi-client support +(multiple tokens sharing one nick/session with independent message queues). +Anonymous sessions created via `POST /api/v1/session` always create a new +user with a new nick. A future endpoint to "add a client to an existing +anonymous session" is planned but not yet implemented. **Rationale:** The fundamental IRC mobile problem is that you can't have your phone and laptop connected simultaneously without a bouncer. Server-side @@ -327,8 +358,8 @@ needs to revoke a token, change the expiry model, or add/remove claims, JWT clients may break or behave incorrectly. Opaque tokens are simpler: -- Server generates 32 random bytes → hex-encodes → stores hash -- Client presents the token; server looks it up +- Server generates 32 random bytes → hex-encodes → stores SHA-256 hash +- Client presents the raw token; server hashes and looks it up - Revocation is a database delete - No clock skew issues, no algorithm confusion, no "none" algorithm attacks - Token format can change without breaking clients @@ -355,6 +386,8 @@ The entire read/write loop for a client is two endpoints. Everything else ### Session Lifecycle +#### Anonymous Session + ``` ┌─ Client ──────────────────────────────────────────────────┐ │ │ @@ -385,6 +418,29 @@ The entire read/write loop for a client is two endpoints. Everything else └────────────────────────────────────────────────────────────┘ ``` +#### Registered Account + +``` +┌─ Client ──────────────────────────────────────────────────┐ +│ │ +│ 1. POST /api/v1/register │ +│ {"nick":"alice", "password":"s3cret!!"} │ +│ → {"id":1, "nick":"alice", "token":"a1b2c3..."} │ +│ (Session created with bcrypt-hashed password) │ +│ │ +│ ... use the API normally (JOIN, PRIVMSG, poll, etc.) ... │ +│ │ +│ (Later, from a new device or after token expiry) │ +│ │ +│ 2. POST /api/v1/login │ +│ {"nick":"alice", "password":"s3cret!!"} │ +│ → {"id":1, "nick":"alice", "token":"d4e5f6..."} │ +│ (New client added to existing session — channels │ +│ and message queues are preserved) │ +│ │ +└────────────────────────────────────────────────────────────┘ +``` + ### Queue Architecture ``` @@ -1034,6 +1090,104 @@ TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \ echo $TOKEN ``` +### POST /api/v1/register — Register Account + +Create a new user session with a password. The password is hashed with bcrypt +and stored server-side. The nick is claimed and can be reclaimed later via +`POST /api/v1/login`. + +**Request Body:** +```json +{"nick": "alice", "password": "mypassword"} +``` + +| Field | Type | Required | Constraints | +|------------|--------|----------|-------------| +| `nick` | string | Yes | 1–32 characters, must be unique on the server | +| `password` | string | Yes | Minimum 8 characters | + +**Response:** `201 Created` +```json +{ + "id": 1, + "nick": "alice", + "token": "494ba9fc0f2242873fc5c285dd4a24fc3844ba5e67789a17e69b6fe5f8c132e3" +} +``` + +| Field | Type | Description | +|---------|---------|-------------| +| `id` | integer | Server-assigned user ID | +| `nick` | string | Confirmed nick | +| `token` | string | 64-character hex auth token | + +**Errors:** + +| Status | Error | When | +|--------|-------|------| +| 400 | `invalid nick format` | Nick doesn't match allowed format | +| 400 | `password must be at least 8 characters` | Password too short | +| 409 | `nick already taken` | Another active session holds this nick | + +**curl example:** +```bash +TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/register \ + -H 'Content-Type: application/json' \ + -d '{"nick":"alice","password":"mypassword"}' | jq -r .token) +echo $TOKEN +``` + +### POST /api/v1/login — Login to Account + +Authenticate with a previously registered nick and password. Creates a new +client token for the existing session, preserving channel memberships and +message queues. This is how multi-client access works for registered accounts: +each login adds a new client to the session. + +On successful login, the server enqueues MOTD messages and synthetic channel +state (JOIN + TOPIC + NAMES for each channel the session belongs to) into the +new client's queue, so the client can immediately restore its UI state. + +**Request Body:** +```json +{"nick": "alice", "password": "mypassword"} +``` + +| Field | Type | Required | Constraints | +|------------|--------|----------|-------------| +| `nick` | string | Yes | Must match a registered account | +| `password` | string | Yes | Must match the account's password | + +**Response:** `200 OK` +```json +{ + "id": 1, + "nick": "alice", + "token": "7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f" +} +``` + +| Field | Type | Description | +|---------|---------|-------------| +| `id` | integer | Session ID (same as when registered) | +| `nick` | string | Current nick | +| `token` | string | New 64-character hex auth token for this client | + +**Errors:** + +| Status | Error | When | +|--------|-------|------| +| 400 | `nick and password required` | Missing nick or password | +| 401 | `invalid credentials` | Wrong password, nick not found, or account has no password | + +**curl example:** +```bash +TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/login \ + -H 'Content-Type: application/json' \ + -d '{"nick":"alice","password":"mypassword"}' | jq -r .token) +echo $TOKEN +``` + ### GET /api/v1/state — Get Session State Return the current user's session state. @@ -1590,9 +1744,16 @@ authenticity. ### Authentication - **Session auth**: Opaque bearer tokens (64 hex chars = 256 bits of entropy). - Tokens are stored in the database and validated on every request. -- **No passwords**: Session creation requires only a nick. The token is the - sole credential. + Tokens are hashed (SHA-256) before storage and validated on every request. +- **Anonymous sessions**: `POST /api/v1/session` requires only a nick. No + password, instant access. The token is the sole credential. +- **Registered accounts**: `POST /api/v1/register` accepts a nick and password + (minimum 8 characters). The password is hashed with bcrypt at the default + cost factor and stored alongside the session. `POST /api/v1/login` + authenticates against the stored hash and issues a new client token. +- **Password security**: Passwords are never stored in plain text. bcrypt + handles salting and key stretching automatically. Anonymous sessions have + an empty `password_hash` and cannot be logged into via `/login`. - **Token security**: Tokens should be treated like session cookies. Transmit only over HTTPS in production. If a token is compromised, the attacker has full access to the session until QUIT or expiry. @@ -1740,13 +1901,26 @@ The database schema is managed via embedded SQL migration files in **Current tables:** -#### `users` +#### `sessions` +| Column | Type | Description | +|----------------|----------|-------------| +| `id` | INTEGER | Primary key (auto-increment) | +| `uuid` | TEXT | Unique session UUID | +| `nick` | TEXT | Unique nick | +| `password_hash`| TEXT | bcrypt hash (empty string for anonymous sessions) | +| `signing_key` | TEXT | Public signing key (empty string if unset) | +| `away_message` | TEXT | Away message (empty string if not away) | +| `created_at` | DATETIME | Session creation time | +| `last_seen` | DATETIME | Last API request time | + +#### `clients` | Column | Type | Description | |-------------|----------|-------------| | `id` | INTEGER | Primary key (auto-increment) | -| `nick` | TEXT | Unique nick | -| `token` | TEXT | Unique auth token (64 hex chars) | -| `created_at`| DATETIME | Session creation time | +| `uuid` | TEXT | Unique client UUID | +| `session_id`| INTEGER | FK → sessions.id (cascade delete) | +| `token` | TEXT | Unique auth token (SHA-256 hash of 64 hex chars) | +| `created_at`| DATETIME | Client creation time | | `last_seen` | DATETIME | Last API request time | #### `channels` @@ -1803,10 +1977,14 @@ skew issues) and simpler than UUIDs (integer comparison vs. string comparison). - **Client output queue entries**: Pruned automatically when older than `QUEUE_MAX_AGE` (default 30 days). - **Channels**: Deleted when the last member leaves (ephemeral). -- **Users/sessions**: Deleted on `QUIT` or `POST /api/v1/logout`. Idle - sessions are automatically expired after `SESSION_IDLE_TIMEOUT` (default - 30 days) — the server runs a background cleanup loop that parts idle users - from all channels, broadcasts QUIT, and releases their nicks. +- **Sessions**: Anonymous sessions are deleted on `QUIT` or when all clients + have logged out. Registered sessions persist across logouts — the session + remains so the user can log in again later. Idle sessions (both anonymous + and registered) are automatically expired after `SESSION_IDLE_TIMEOUT` + (default 30 days) — the server runs a background cleanup loop that parts + idle users from all channels, broadcasts QUIT, and releases their nicks. +- **Clients**: Individual client tokens are deleted on `POST /api/v1/logout`. + A session can have multiple clients; removing one doesn't affect others. --- @@ -1955,11 +2133,21 @@ A complete client needs only four HTTP calls: ### Step-by-Step with curl ```bash -# 1. Create a session +# 1a. Create an anonymous session (no account) export TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \ -H 'Content-Type: application/json' \ -d '{"nick":"testuser"}' | jq -r .token) +# 1b. Or register an account (persistent identity) +export TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/register \ + -H 'Content-Type: application/json' \ + -d '{"nick":"testuser","password":"mypassword"}' | jq -r .token) + +# 1c. Or login to an existing account +export TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/login \ + -H 'Content-Type: application/json' \ + -d '{"nick":"testuser","password":"mypassword"}' | jq -r .token) + # 2. Join a channel curl -s -X POST http://localhost:8080/api/v1/messages \ -H "Authorization: Bearer $TOKEN" \ @@ -2092,9 +2280,11 @@ Clients should handle these message commands from the queue: ### Error Handling -- **HTTP 401**: Token expired or invalid. Re-create session. +- **HTTP 401**: Token expired or invalid. Re-create session (anonymous) or + re-login (registered account). - **HTTP 404**: Channel or user not found. -- **HTTP 409**: Nick already taken (on session creation or NICK change). +- **HTTP 409**: Nick already taken (on session creation, registration, or + NICK change). - **HTTP 400**: Malformed request. Check the `error` field in the response. - **Network errors**: Back off exponentially (1s, 2s, 4s, ..., max 30s). @@ -2111,8 +2301,10 @@ Clients should handle these message commands from the queue: 4. **DM tab logic**: When you receive a PRIVMSG where `to` is not a channel (no `#` prefix), the DM tab should be keyed by the **other** user's nick: if `from` is you, use `to`; if `from` is someone else, use `from`. -5. **Reconnection**: If the poll loop fails with 401, the session is gone. - Create a new session. If it fails with a network error, retry with backoff. +5. **Reconnection**: If the poll loop fails with 401, the token is invalid. + For anonymous sessions, create a new session. For registered accounts, + log in again via `POST /api/v1/login` to get a fresh token on the same + session. If it fails with a network error, retry with backoff. --- @@ -2383,9 +2575,12 @@ neoirc/ build a working IRC-style TUI client against this API in an afternoon, the API is too complex. -2. **No accounts** — identity is a signing key, nick is a display name. No - registration, no passwords, no email verification. Session creation is - instant. The cost of entry is a hashcash proof, not bureaucracy. +2. **Accounts optional** — anonymous sessions are instant: pick a nick and + talk. No registration, no email verification. The cost of entry is a + hashcash proof, not bureaucracy. For users who want persistent identity, + optional account registration with password is available — but never + required. Identity verification at the message layer uses cryptographic + signing, independent of account status. 3. **IRC semantics over HTTP** — command names and numeric codes from RFC 1459/2812. If you've built an IRC client or bot, you already know the