fix: correct all documentation inaccuracies about cookie-based auth
All checks were successful
check / check (push) Successful in 2m14s

- Fix false claim 'clients never need to handle the token directly' —
  CLI clients (curl, custom HTTP clients) must explicitly manage cookies
- Replace 'token' with 'cookie' in multi-client diagram (token_a → cookie_a)
- Fix Set-Cookie placeholders in protocol diagrams (<token> → <random_hex>/<cookie_a>/<cookie_b>)
- Fix 'old token' → 'old auth cookie' in QUIT command description
- Fix 'get token' → 'get auth cookie' in Client Development Guide
- Fix 'Tokens are hashed' → 'Cookie values are hashed' in Security Model
- Fix 'client tokens are deleted' → 'client auth cookies are invalidated'
- Fix 'Cookie sent automatically' → 'Cookie must be sent' in diagram
- Fix 'eliminates token management from client code entirely' rationale
- Fix 'No token appears in the JSON body' → 'No auth credential appears'
- Fix 'encoded in the token' → 'encoded in the cookie value'
- Fix 'Clients never handle tokens directly' in JWT comparison section
- Update clients table token column description for clarity
- All remaining 'token' refs verified as legitimate (pow_token/hashcash/JWT comparison/DB schema column name)
This commit is contained in:
clawbot
2026-03-19 23:17:49 -07:00
parent 73c92a2651
commit 61aa678492
4 changed files with 49 additions and 41 deletions

View File

@@ -159,11 +159,13 @@ for multi-client access.
- **Session creation**: client sends `POST /api/v1/session` with a desired - **Session creation**: client sends `POST /api/v1/session` with a desired
nick → server sets an **HttpOnly auth cookie** (`neoirc_auth`) containing nick → server sets an **HttpOnly auth cookie** (`neoirc_auth`) containing
a cryptographically random token (64 hex characters) and returns the user a cryptographically random value (64 hex characters) and returns the user
ID and nick in the JSON response body. No token appears in the JSON body. ID and nick in the JSON response body. No auth credential appears in the
JSON body.
- The auth cookie is HttpOnly, SameSite=Strict, and Secure when behind TLS. - The auth cookie is HttpOnly, SameSite=Strict, and Secure when behind TLS.
Clients never need to handle the token directly — the browser/HTTP client Browsers handle cookies automatically. **CLI clients (curl, custom HTTP
manages cookies automatically. clients) must explicitly save and send cookies** — e.g., using curl's
`-c`/`-b` flags or an HTTP cookie jar in their language's HTTP library.
- Sessions start anonymous — no password required. When the session expires - Sessions start anonymous — no password required. When the session expires
or the user QUITs, the nick is released. or the user QUITs, the nick is released.
@@ -188,16 +190,17 @@ For users who want to access the same session from multiple devices:
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.
- Auth cookies contain opaque random bytes, **not JWTs**. No claims, no expiry - Auth cookies contain opaque random bytes, **not JWTs**. No claims, no expiry
encoded in the token, no client-side decode. The server is the sole authority encoded in the cookie value, no client-side decode. The server is the sole
on cookie validity. authority on cookie validity.
**Rationale:** IRC has no accounts. You connect, pick a nick, and talk. **Rationale:** IRC has no accounts. You connect, pick a nick, and talk.
Anonymous sessions preserve that simplicity — instant access, zero friction. Anonymous sessions preserve that simplicity — instant access, zero friction.
But some users want to access the same session from multiple devices without But some users want to access the same session from multiple devices without
a bouncer. The PASS command enables multi-client login without adding friction a bouncer. The PASS command enables multi-client login without adding friction
for casual users: if you don't need multi-client, just create a session and for casual users: if you don't need multi-client, just create a session and
go. Cookie-based auth eliminates token management from client code entirely — go. Cookie-based auth simplifies credential management — browsers handle
browsers and HTTP cookie jars handle it automatically. Note: both anonymous cookies automatically, and CLI clients just need a cookie jar (e.g., curl's
`-c`/`-b` flags). Note: both anonymous
and password-protected sessions are deleted when the last client disconnects and password-protected sessions are deleted when the last client disconnects
(QUIT or logout). Identity verification at the message layer via cryptographic (QUIT or logout). Identity verification at the message layer via cryptographic
signatures (see [Security Model](#security-model)) remains independent of signatures (see [Security Model](#security-model)) remains independent of
@@ -265,9 +268,9 @@ A single user session can have multiple clients (phone, laptop, terminal).
``` ```
User Session User Session
├── Client A (token_a, queue_a) ├── Client A (cookie_a, queue_a)
├── Client B (token_b, queue_b) ├── Client B (cookie_b, queue_b)
└── Client C (token_c, queue_c) └── Client C (cookie_c, queue_c)
``` ```
**Multi-client via login:** The `POST /api/v1/login` endpoint adds a new **Multi-client via login:** The `POST /api/v1/login` endpoint adds a new
@@ -395,8 +398,8 @@ Opaque auth cookies are simpler:
- Revocation is a database delete (cookie becomes invalid immediately) - Revocation is a database delete (cookie becomes invalid immediately)
- No clock skew issues, no algorithm confusion, no "none" algorithm attacks - No clock skew issues, no algorithm confusion, no "none" algorithm attacks
- Cookie format can change without breaking clients - Cookie format can change without breaking clients
- Clients never handle tokens directly — browsers and HTTP cookie jars - Browsers and HTTP cookie jars manage cookies automatically; CLI clients
manage everything automatically must explicitly save and resend cookies (e.g., curl `-c`/`-b` flags)
--- ---
@@ -426,12 +429,12 @@ The entire read/write loop for a client is two endpoints. Everything else
┌─ Client ──────────────────────────────────────────────────┐ ┌─ Client ──────────────────────────────────────────────────┐
│ │ │ │
│ 1. POST /api/v1/session {"nick":"alice"} │ │ 1. POST /api/v1/session {"nick":"alice"} │
│ → Set-Cookie: neoirc_auth=<token>; HttpOnly; ... │ → Set-Cookie: neoirc_auth=<random_hex>; HttpOnly; ... │
│ → {"id":1, "nick":"alice"} │ │ → {"id":1, "nick":"alice"} │
│ │ │ │
│ 2. POST /api/v1/messages {"command":"JOIN","to":"#gen"} │ │ 2. POST /api/v1/messages {"command":"JOIN","to":"#gen"} │
│ → {"status":"joined","channel":"#general"} │ │ → {"status":"joined","channel":"#general"} │
│ (Cookie sent automatically on all subsequent requests) │ │ (Cookie must be sent on all subsequent requests)
│ │ │ │
│ 3. POST /api/v1/messages {"command":"PRIVMSG", │ │ 3. POST /api/v1/messages {"command":"PRIVMSG", │
│ "to":"#general","body":["hello"]} │ │ "to":"#general","body":["hello"]} │
@@ -459,7 +462,7 @@ The entire read/write loop for a client is two endpoints. Everything else
┌─ Client A ────────────────────────────────────────────────┐ ┌─ Client A ────────────────────────────────────────────────┐
│ │ │ │
│ 1. POST /api/v1/session {"nick":"alice"} │ │ 1. POST /api/v1/session {"nick":"alice"} │
│ → Set-Cookie: neoirc_auth=<token_a>; HttpOnly; ... │ → Set-Cookie: neoirc_auth=<cookie_a>; HttpOnly; ... │
│ → {"id":1, "nick":"alice"} │ │ → {"id":1, "nick":"alice"} │
│ │ │ │
│ 2. POST /api/v1/messages │ │ 2. POST /api/v1/messages │
@@ -475,7 +478,7 @@ The entire read/write loop for a client is two endpoints. Everything else
│ │ │ │
│ 3. POST /api/v1/login │ │ 3. POST /api/v1/login │
│ {"nick":"alice", "password":"s3cret!!"} │ │ {"nick":"alice", "password":"s3cret!!"} │
│ → Set-Cookie: neoirc_auth=<token_b>; HttpOnly; ... │ → Set-Cookie: neoirc_auth=<cookie_b>; HttpOnly; ... │
│ → {"id":1, "nick":"alice"} │ │ → {"id":1, "nick":"alice"} │
│ (New client added to existing session — channels │ │ (New client added to existing session — channels │
│ and message queues are preserved.) │ │ and message queues are preserved.) │
@@ -862,7 +865,7 @@ Destroy the session and disconnect from the server.
- Empty channels are deleted (ephemeral). - Empty channels are deleted (ephemeral).
- The user's session is destroyed — the auth cookie is invalidated, the nick - The user's session is destroyed — the auth cookie is invalidated, the nick
is released. is released.
- Subsequent requests with the old token return HTTP 401. - Subsequent requests with the old auth cookie return HTTP 401.
**Response:** `200 OK` **Response:** `200 OK`
```json ```json
@@ -1226,8 +1229,8 @@ the hostmask used in WHOIS, WHO, and future ban matching (`+b`).
**Response:** `201 Created` **Response:** `201 Created`
The response sets an `neoirc_auth` HttpOnly cookie containing the auth token. The response sets an `neoirc_auth` HttpOnly cookie containing an opaque auth
The JSON body does **not** include the token. value. The JSON body does **not** include the auth credential.
``` ```
Set-Cookie: neoirc_auth=494ba9fc...e3; Path=/; HttpOnly; SameSite=Strict Set-Cookie: neoirc_auth=494ba9fc...e3; Path=/; HttpOnly; SameSite=Strict
@@ -1914,8 +1917,8 @@ authenticity.
### Authentication ### Authentication
- **Cookie-based auth**: Opaque HttpOnly cookies (64 hex chars = 256 bits of - **Cookie-based auth**: Opaque HttpOnly cookies (64 hex chars = 256 bits of
entropy). Tokens are hashed (SHA-256) before storage and validated on every entropy). Cookie values are hashed (SHA-256) before storage and validated on
request. Cookies are HttpOnly (no JavaScript access), SameSite=Strict every request. Cookies are HttpOnly (no JavaScript access), SameSite=Strict
(CSRF protection), and Secure when behind TLS. (CSRF protection), and Secure when behind TLS.
- **Anonymous sessions**: `POST /api/v1/session` requires only a nick. No - **Anonymous sessions**: `POST /api/v1/session` requires only a nick. No
password, instant access. The auth cookie is the sole credential. password, instant access. The auth cookie is the sole credential.
@@ -2094,11 +2097,11 @@ Index on `(uuid)`.
#### `clients` #### `clients`
| Column | Type | Description | | Column | Type | Description |
|-------------|----------|-------------| |--------------|----------|-------------|
| `id` | INTEGER | Primary key (auto-increment) | | `id` | INTEGER | Primary key (auto-increment) |
| `uuid` | TEXT | Unique client UUID | | `uuid` | TEXT | Unique client UUID |
| `session_id` | INTEGER | FK → sessions.id (cascade delete) | | `session_id` | INTEGER | FK → sessions.id (cascade delete) |
| `token` | TEXT | Unique auth token (SHA-256 hash of 64 hex chars) | | `token` | TEXT | Auth cookie value (SHA-256 hash of the 64-hex-char cookie) |
| `ip` | TEXT | Real IP address of this client connection | | `ip` | TEXT | Real IP address of this client connection |
| `hostname` | TEXT | Reverse DNS hostname of this client connection | | `hostname` | TEXT | Reverse DNS hostname of this client connection |
| `created_at` | DATETIME | Client creation time | | `created_at` | DATETIME | Client creation time |
@@ -2171,7 +2174,7 @@ skew issues) and simpler than UUIDs (integer comparison vs. string comparison).
after `SESSION_IDLE_TIMEOUT` after `SESSION_IDLE_TIMEOUT`
(default 30 days) — the server runs a background cleanup loop that parts (default 30 days) — the server runs a background cleanup loop that parts
idle users from all channels, broadcasts QUIT, and releases their nicks. idle users from all channels, broadcasts QUIT, and releases their nicks.
- **Clients**: Individual client tokens are deleted on `POST /api/v1/logout`. - **Clients**: Individual client auth cookies are invalidated on `POST /api/v1/logout`.
A session can have multiple clients; removing one doesn't affect others. A session can have multiple clients; removing one doesn't affect others.
However, when the last client is removed (via logout), the entire session However, when the last client is removed (via logout), the entire session
is deleted — the user is parted from all channels, QUIT is broadcast, and is deleted — the user is parted from all channels, QUIT is broadcast, and
@@ -2317,7 +2320,7 @@ with an HTTP client library.
A complete client needs only four HTTP calls: A complete client needs only four HTTP calls:
``` ```
1. POST /api/v1/session → get token 1. POST /api/v1/session → get auth cookie
2. POST /api/v1/messages (JOIN) → join channels 2. POST /api/v1/messages (JOIN) → join channels
3. GET /api/v1/messages (loop) → receive messages 3. GET /api/v1/messages (loop) → receive messages
4. POST /api/v1/messages → send messages 4. POST /api/v1/messages → send messages

View File

@@ -335,7 +335,7 @@ func (hdlr *Handlers) executeCreateSession(
hdlr.respondJSON(writer, request, map[string]any{ hdlr.respondJSON(writer, request, map[string]any{
"id": sessionID, "id": sessionID,
"nick": payload.Nick, "nick": nick,
}, http.StatusCreated) }, http.StatusCreated)
} }

View File

@@ -2230,7 +2230,7 @@ func TestWhoisShowsHostInfo(t *testing.T) {
} }
// createSessionWithUsername creates a session with a // createSessionWithUsername creates a session with a
// specific username and returns the token. // specific username and returns the auth cookie value.
func (tserver *testServer) createSessionWithUsername( func (tserver *testServer) createSessionWithUsername(
nick, username string, nick, username string,
) string { ) string {
@@ -2264,13 +2264,19 @@ func (tserver *testServer) createSessionWithUsername(
) )
} }
var result struct { // Drain the body.
Token string `json:"token"` _, _ = io.ReadAll(resp.Body)
// Extract auth cookie from response.
for _, cookie := range resp.Cookies() {
if cookie.Name == authCookieName {
return cookie.Value
}
} }
_ = json.NewDecoder(resp.Body).Decode(&result) tserver.t.Fatal("no auth cookie in response")
return result.Token return ""
} }
func TestWhoShowsHostInfo(t *testing.T) { func TestWhoShowsHostInfo(t *testing.T) {

View File

@@ -10,7 +10,6 @@ import (
const minPasswordLength = 8 const minPasswordLength = 8
// HandleLogin authenticates a user with nick and password. // HandleLogin authenticates a user with nick and password.
func (hdlr *Handlers) HandleLogin() http.HandlerFunc { func (hdlr *Handlers) HandleLogin() http.HandlerFunc {
return func( return func(