docs: document register/login and dual authentication model
All checks were successful
check / check (push) Successful in 1m8s
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:
269
README.md
269
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 <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
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user