--- phase: 01-tournament-engine plan: 03 subsystem: auth-audit tags: [jwt, bcrypt, pin-auth, rate-limiting, audit-trail, undo-engine, hs256, nats-jetstream] # Dependency graph requires: - phase: 01-01 provides: HTTP server, NATS JetStream, WebSocket hub - phase: 01-02 provides: Database schema (operators, audit_entries, login_attempts tables) provides: - PIN-based operator authentication with JWT issuance - HS256-enforced JWT validation middleware with role extraction - Auth HTTP routes (login, me, logout) - Append-only audit trail with LibSQL persistence and NATS publishing - Undo engine with reversal entries and double-undo protection - Auth-to-audit bridge via RecorderFunc callback affects: [01-tournament-engine, financial-engine, player-management, clock-engine] # Tech tracking tech-stack: added: [golang-jwt-v5, bcrypt] patterns: [pin-scan-auth, rate-limiting-libsql, audit-recorder-callback, append-only-audit, reversal-entries] key-files: created: - internal/auth/pin_test.go - internal/server/routes/auth.go - internal/audit/trail_test.go - internal/audit/undo_test.go modified: - internal/auth/jwt.go - internal/auth/pin.go - internal/audit/trail.go - internal/audit/undo.go - internal/server/middleware/auth.go - internal/server/server.go - cmd/leaf/main.go - cmd/leaf/main_test.go key-decisions: - "JWT HS256 enforcement via jwt.WithValidMethods prevents algorithm confusion attacks" - "Rate limiting keyed by global sentinel (_global) since PINs are scanned across all operators" - "AuditRecorder callback type breaks import cycle between auth and audit packages" - "Audit timestamps stored as epoch seconds in SQLite, converted to/from nanoseconds in Go" - "NATS publish is best-effort (logged, not fatal) to avoid audit trail failures blocking mutations" - "Undo creates reversal entry (never deletes) -- only exception is marking undone_by on original" patterns-established: - "Auth middleware extracts JWT claims to context (OperatorIDKey, OperatorRoleKey)" - "Audit recorder as callback function to decouple auth package from audit package" - "Mock publisher interface for testing audit NATS integration without real NATS" - "Rate limit thresholds: 5 failures = 30s, 8 = 5min, 10 = 30min lockout" requirements-completed: [AUTH-01, AUTH-03, ARCH-08, PLYR-06] # Metrics duration: 5min completed: 2026-03-01 --- # Phase 1 Plan 03: Authentication + Audit Trail + Undo Engine Summary **PIN auth with HS256 JWT, bcrypt rate limiting in LibSQL, append-only audit trail with NATS publishing, and reversal-based undo engine** ## Performance - **Duration:** 5 min - **Started:** 2026-03-01T02:58:26Z - **Completed:** 2026-03-01T03:03:22Z - **Tasks:** 2 - **Files modified:** 12 ## Accomplishments - PIN-based authentication scanning all operators via bcrypt, issuing HS256 JWT with role claims (admin/floor/viewer) - Rate limiting persisted in LibSQL login_attempts table with exponential backoff (5/8/10 failure thresholds) - JWT validation enforces HS256 via WithValidMethods to prevent algorithm confusion attacks - Auth HTTP routes: POST /api/v1/auth/login, GET /api/v1/auth/me, POST /api/v1/auth/logout - JWT signing key generated on first startup and persisted in _config table - Append-only audit trail with 30+ action constants covering all domain mutations - NATS JetStream publishing for tournament-scoped audit events (best-effort, non-blocking) - Undo engine creating reversal entries that swap previous/new state -- never deletes originals - Double-undo protection via undone_by field (tamper-protected by SQLite triggers) - RecorderFunc bridges auth package to audit trail without import cycles - 11 unit tests for auth package, 6 integration tests for auth HTTP endpoints - 10 audit trail tests, 8 undo engine tests -- all passing ## Task Commits Each task was committed atomically: 1. **Task C1: Implement PIN authentication with JWT issuance** - `dd2f9bb` (feat) - Auth routes, HS256 enforcement, context helpers, comprehensive test suite 2. **Task C2: Implement audit trail and undo engine** - Previously committed in `1978d3d` - Trail, undo engine, action constants, tests were auto-populated during Plan 05 execution ## Files Created/Modified - `internal/auth/jwt.go` - JWT service with HS256 enforcement, signing key persistence - `internal/auth/pin.go` - AuthService with PIN login, rate limiting, operator CRUD - `internal/auth/pin_test.go` - 11 comprehensive auth unit tests - `internal/server/middleware/auth.go` - JWT middleware with WithValidMethods, context helpers - `internal/server/middleware/role.go` - Role hierarchy middleware (admin > floor > viewer) - `internal/server/routes/auth.go` - Auth HTTP handlers (login, me, logout) - `internal/audit/trail.go` - AuditTrail with LibSQL persistence, NATS publishing, 30+ action constants - `internal/audit/undo.go` - UndoEngine with reversal entries, undoable action whitelist - `internal/audit/trail_test.go` - 10 audit trail tests (persistence, NATS, pagination, filtering) - `internal/audit/undo_test.go` - 8 undo engine tests (reversal, double-undo, non-undoable actions) - `internal/server/server.go` - Updated with authService and clockRegistry parameters - `cmd/leaf/main.go` - Auth service creation with persisted signing key ## Decisions Made - **HS256 enforcement:** JWT validation uses both method type check and WithValidMethods([]string{"HS256"}) -- belt AND suspenders against algorithm confusion attacks. - **Global rate limiting key:** Since PINs are compared against all operators (scan), failures are recorded against a "_global" sentinel key rather than per-operator. Resets on any successful login. - **Audit recorder callback:** The auth package defines an AuditRecorder function type and calls it for login events. The audit package provides RecorderFunc() that creates this callback. This avoids circular imports. - **Best-effort NATS publishing:** Audit trail records to LibSQL first (source of truth), then publishes to NATS. NATS failures are logged but don't fail the operation -- prevents audit infrastructure issues from blocking game actions. - **Timestamp dual representation:** SQLite stores epoch seconds (schema default). Go layer converts to/from nanoseconds for precision. GetEntry/GetEntries multiply by 1e9 when reading. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Task C2 code already committed by previous session** - **Found during:** Task C2 staging - **Issue:** The audit trail and undo engine code (trail.go, undo.go, trail_test.go, undo_test.go) had been auto-populated and committed in commit `1978d3d` during Plan 05 execution. My Write operations produced identical code to what was already committed, resulting in zero git diff. - **Fix:** Verified all 18 audit tests pass. No commit needed since code was already in the repository. - **Impact:** None -- the implementation matches the plan requirements exactly. No other deviations. Plan executed as written. ## Verification Results 1. PIN login with "1234" produces valid JWT with role claims -- PASS 2. Auth middleware rejects requests without valid JWT (401) -- PASS 3. Role middleware enforces admin/floor/viewer hierarchy -- PASS 4. Rate limiting activates after 5 failed login attempts -- PASS 5. Audit entries persist to LibSQL with all fields -- PASS 6. NATS JetStream receives audit events on correct subject -- PASS (mock) 7. Undo creates reversal entry and marks original -- PASS 8. Double-undo is rejected with clear error -- PASS 9. All 35 tests pass (11 auth + 6 integration + 10 trail + 8 undo) -- PASS ## Next Phase Readiness - Auth middleware available for all protected routes in subsequent plans - Audit trail ready as cross-cutting concern for financial, player, and seat mutations - Undo engine ready for financial transactions and bust-out reversals - Rate limiting and signing key persistence operational - Ready for Plan F (Financial Engine), Plan G (Player Management) ## Self-Check: PASSED - All 12 key files verified present - Task C1 commit (dd2f9bb) verified in git log - Task C2 code verified present (committed in 1978d3d) - All 35 tests pass --- *Phase: 01-tournament-engine* *Completed: 2026-03-01*