From 534bbe571fff976b3fde33581242b3cb4824da7a Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 20 Feb 2026 07:56:35 +0000 Subject: [PATCH] docs: flesh out Phase 2 auth task specs Detail Authenticator interface, APIKeyAuthenticator built-in, HubConfig integration (nil = backward compat), Client auth fields, OnAuthFailure callback, and full test matrix with integration tests. Co-Authored-By: Virgil --- TODO.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index f9d4604..463b71e 100644 --- a/TODO.md +++ b/TODO.md @@ -20,9 +20,35 @@ Dispatched from core/go orchestration. Pick up tasks in order. ## Phase 2: Auth -- [ ] Add token-based authentication on WebSocket upgrade handshake -- [ ] Validate JWT or API key before promoting HTTP connection to WebSocket -- [ ] Reject unauthenticated connections with appropriate HTTP status +Token-based authentication on WebSocket upgrade handshake. Pure Go, no JWT library dependency — consumers bring their own validation logic via an interface. + +### 2.1 Authenticator Interface + +- [ ] **Create `auth.go`** — Define the auth abstraction: + - `type AuthResult struct { Valid bool; UserID string; Claims map[string]any; Error error }` — result of authentication + - `type Authenticator interface { Authenticate(r *http.Request) AuthResult }` — validates the HTTP request during upgrade. Implementations can check headers (`Authorization: Bearer `), query params (`?token=xxx`), or cookies. + - `type AuthenticatorFunc func(r *http.Request) AuthResult` — adapter for using functions as Authenticators (implements the interface) + - `type APIKeyAuthenticator struct { Keys map[string]string }` — built-in authenticator that validates `Authorization: Bearer ` against a static key→userID map. Provided as a convenience; consumers can use their own JWT-based authenticator. + - `func NewAPIKeyAuth(keys map[string]string) *APIKeyAuthenticator` — constructor + +### 2.2 Wire Into Hub + +- [ ] **Add `Authenticator` to `HubConfig`** — Optional field. When nil, all connections are accepted (backward compatible). When set, `Handler()` calls `Authenticate(r)` before upgrading. +- [ ] **Update `Handler()`** — If `h.config.Authenticator != nil`, call `Authenticate(r)`. If `!result.Valid`, respond with `http.StatusUnauthorized` (or `http.StatusForbidden` if `result.Error` indicates a different status) and return without upgrading. If valid, store `result.UserID` and `result.Claims` on the `Client` struct. +- [ ] **Add auth fields to `Client`** — `UserID string` and `Claims map[string]any` fields. Set during authenticated upgrade. Empty for unauthenticated hubs (nil authenticator). +- [ ] **Expose `OnAuthFailure` callback** — Optional `OnAuthFailure func(r *http.Request, result AuthResult)` on `HubConfig` for logging/metrics on rejected connections. + +### 2.3 Tests + +- [ ] **Unit tests** — (a) APIKeyAuthenticator valid key, (b) invalid key, (c) missing header, (d) malformed header ("Bearer" without token, wrong scheme), (e) AuthenticatorFunc adapter, (f) nil Authenticator (backward compat — all connections accepted) +- [ ] **Integration tests** — Using httptest + gorilla/websocket Dial: + - (a) Authenticated connect with valid API key → upgrade succeeds, client.UserID set + - (b) Rejected connect with invalid key → HTTP 401, no WebSocket upgrade + - (c) Rejected connect with no auth header → HTTP 401 + - (d) Nil authenticator → all connections accepted (existing behaviour preserved) + - (e) OnAuthFailure callback fires on rejection + - (f) Multiple clients with different API keys → each gets correct UserID +- [ ] **Existing tests still pass** — No authenticator set = backward compatible ## Phase 3: Scaling