docs: document register/login and dual authentication model
All checks were successful
check / check (push) Successful in 1m8s

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
This commit is contained in:
clawbot
2026-03-17 02:17:59 -07:00
parent cab5784913
commit 0900289af5

269
README.md
View File

@@ -113,8 +113,9 @@ mechanisms or stuffing data into CTCP.
Everything else is IRC. `PRIVMSG`, `JOIN`, `PART`, `NICK`, `TOPIC`, `MODE`, Everything else is IRC. `PRIVMSG`, `JOIN`, `PART`, `NICK`, `TOPIC`, `MODE`,
`KICK`, `353`, `433` — same commands, same semantics. Channels start with `#`. `KICK`, `353`, `433` — same commands, same semantics. Channels start with `#`.
Joining a nonexistent channel creates it. Channels disappear when empty. Nicks 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 are unique per server. Identity starts with a key a nick is a display name.
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 ### 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 IRC's command model is correct and only the transport and session management
need to change. need to change.
### Identity & Sessions — No Accounts ### Identity & Sessions — Dual Authentication Model
There are no accounts, no registration, no passwords. Identity is a signing The server supports two authentication paths: **anonymous sessions** for
key; a nick is just a display name. The two are decoupled. 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 - **Session creation**: client sends `POST /api/v1/session` with a desired
nick → server assigns an **auth token** (64 hex characters of nick → server assigns an **auth token** (64 hex characters of
cryptographically random bytes) and returns the user ID, nick, and token. cryptographically random bytes) and returns the user ID, nick, and token.
- The auth token implicitly identifies the client. Clients present it via - The auth token implicitly identifies the client. Clients present it via
`Authorization: Bearer <token>`. `Authorization: Bearer <token>`.
- 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 - Nicks are changeable via the `NICK` command; the server-assigned user ID is
the stable identity. the stable identity.
- Server-assigned IDs — clients do not choose their own IDs. - 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 in the token, no client-side decode. The server is the sole authority on
token validity. token validity.
**Rationale:** IRC has no accounts. You connect, pick a nick, and talk. Adding **Rationale:** IRC has no accounts. You connect, pick a nick, and talk.
registration, email verification, or OAuth would solve a problem nobody asked Anonymous sessions preserve that simplicity — instant access, zero friction.
about and add complexity that drives away casual users. Identity verification But some users want to keep their nick across sessions without relying on a
is handled at the message layer via cryptographic signatures (see bouncer or nick reservation system. Optional registration with password
[Security Model](#security-model)), not at the session layer. 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 ### Nick Semantics
@@ -207,12 +238,12 @@ User Session
└── Client C (token_c, queue_c) └── Client C (token_c, queue_c)
``` ```
**Current MVP note:** The current implementation creates a new user (with new **Multi-client via login:** The `POST /api/v1/login` endpoint adds a new
nick) per `POST /api/v1/session` call. True multi-client (multiple tokens client to an existing registered session, enabling true multi-client support
sharing one nick/session) is supported by the schema (`client_queues` is keyed (multiple tokens sharing one nick/session with independent message queues).
by user_id, and multiple tokens can point to the same user) but the session Anonymous sessions created via `POST /api/v1/session` always create a new
creation endpoint does not yet support "add a client to an existing session." user with a new nick. A future endpoint to "add a client to an existing
This will be added post-MVP. anonymous session" is planned but not yet implemented.
**Rationale:** The fundamental IRC mobile problem is that you can't have your **Rationale:** The fundamental IRC mobile problem is that you can't have your
phone and laptop connected simultaneously without a bouncer. Server-side 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. clients may break or behave incorrectly.
Opaque tokens are simpler: Opaque tokens are simpler:
- Server generates 32 random bytes → hex-encodes → stores hash - Server generates 32 random bytes → hex-encodes → stores SHA-256 hash
- Client presents the token; server looks it up - Client presents the raw token; server hashes and looks it up
- Revocation is a database delete - Revocation is a database delete
- No clock skew issues, no algorithm confusion, no "none" algorithm attacks - No clock skew issues, no algorithm confusion, no "none" algorithm attacks
- Token format can change without breaking clients - 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 ### Session Lifecycle
#### Anonymous Session
``` ```
┌─ Client ──────────────────────────────────────────────────┐ ┌─ 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 ### Queue Architecture
``` ```
@@ -1034,6 +1090,104 @@ TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \
echo $TOKEN 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 | 132 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 ### GET /api/v1/state — Get Session State
Return the current user's session state. Return the current user's session state.
@@ -1590,9 +1744,16 @@ authenticity.
### Authentication ### Authentication
- **Session auth**: Opaque bearer tokens (64 hex chars = 256 bits of entropy). - **Session auth**: Opaque bearer tokens (64 hex chars = 256 bits of entropy).
Tokens are stored in the database and validated on every request. Tokens are hashed (SHA-256) before storage and validated on every request.
- **No passwords**: Session creation requires only a nick. The token is the - **Anonymous sessions**: `POST /api/v1/session` requires only a nick. No
sole credential. 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 - **Token security**: Tokens should be treated like session cookies. Transmit
only over HTTPS in production. If a token is compromised, the attacker has only over HTTPS in production. If a token is compromised, the attacker has
full access to the session until QUIT or expiry. 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:** **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 | | Column | Type | Description |
|-------------|----------|-------------| |-------------|----------|-------------|
| `id` | INTEGER | Primary key (auto-increment) | | `id` | INTEGER | Primary key (auto-increment) |
| `nick` | TEXT | Unique nick | | `uuid` | TEXT | Unique client UUID |
| `token` | TEXT | Unique auth token (64 hex chars) | | `session_id`| INTEGER | FK → sessions.id (cascade delete) |
| `created_at`| DATETIME | Session creation time | | `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 | | `last_seen` | DATETIME | Last API request time |
#### `channels` #### `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 - **Client output queue entries**: Pruned automatically when older than
`QUEUE_MAX_AGE` (default 30 days). `QUEUE_MAX_AGE` (default 30 days).
- **Channels**: Deleted when the last member leaves (ephemeral). - **Channels**: Deleted when the last member leaves (ephemeral).
- **Users/sessions**: Deleted on `QUIT` or `POST /api/v1/logout`. Idle - **Sessions**: Anonymous sessions are deleted on `QUIT` or when all clients
sessions are automatically expired after `SESSION_IDLE_TIMEOUT` (default have logged out. Registered sessions persist across logouts — the session
30 days) — the server runs a background cleanup loop that parts idle users remains so the user can log in again later. Idle sessions (both anonymous
from all channels, broadcasts QUIT, and releases their nicks. 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 ### Step-by-Step with curl
```bash ```bash
# 1. Create a session # 1a. Create an anonymous session (no account)
export TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \ export TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/session \
-H 'Content-Type: application/json' \ -H 'Content-Type: application/json' \
-d '{"nick":"testuser"}' | jq -r .token) -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 # 2. Join a channel
curl -s -X POST http://localhost:8080/api/v1/messages \ curl -s -X POST http://localhost:8080/api/v1/messages \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
@@ -2092,9 +2280,11 @@ Clients should handle these message commands from the queue:
### Error Handling ### 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 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. - **HTTP 400**: Malformed request. Check the `error` field in the response.
- **Network errors**: Back off exponentially (1s, 2s, 4s, ..., max 30s). - **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 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: (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`. 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. 5. **Reconnection**: If the poll loop fails with 401, the token is invalid.
Create a new session. If it fails with a network error, retry with backoff. 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 build a working IRC-style TUI client against this API in an afternoon, the
API is too complex. API is too complex.
2. **No accounts** — identity is a signing key, nick is a display name. No 2. **Accounts optional** — anonymous sessions are instant: pick a nick and
registration, no passwords, no email verification. Session creation is talk. No registration, no email verification. The cost of entry is a
instant. The cost of entry is a hashcash proof, not bureaucracy. 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 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 RFC 1459/2812. If you've built an IRC client or bot, you already know the