fix: correct all documentation inaccuracies about cookie-based auth
All checks were successful
check / check (push) Successful in 2m14s
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:
71
README.md
71
README.md
@@ -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.
|
||||||
@@ -2093,16 +2096,16 @@ The database schema is managed via embedded SQL migration files in
|
|||||||
Index on `(uuid)`.
|
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 |
|
||||||
| `last_seen` | DATETIME | Last API request time |
|
| `last_seen` | DATETIME | Last API request time |
|
||||||
|
|
||||||
Indexes on `(token)` and `(session_id)`.
|
Indexes on `(token)` and `(session_id)`.
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user