--- phase: 09-backend-admin-authentication-and-schema-pipeline plan: 02 subsystem: auth tags: [jwt, postgres, gorm, admin, cabana, bcrypt, bonfire] requires: - phase: 09-backend-admin-authentication-and-schema-pipeline provides: backend audience guard, cabana login, and the first backend identity migration provides: - Idempotent Winter-shaped backend_users and backend_user_roles migrations plus a separate admin jti table - Login, refresh, logout, and me with sliding refresh, opaque failures, throttle, and redacted auth logs - admin:create and admin:reset-password on the generated binary affects: [09-backend-admin-authentication-and-schema-pipeline, admin-api, phase-10-spa] actuals: tokens: 18249 tasks: 3 commits: 6 tech-stack: added: [] patterns: - "Admin revocation uses backend_jwt_blacklist through bouncer.PostgresBlacklist and does not replace the frontend blacklist" - "Role code is indexed, not unique, so a copied Winter row can repeat a code and admin:create rejects the ambiguous match" - "Password reset advances tokens_valid_after so existing backend JWTs fail closed" key-files: created: - lagoon/backend_admin_migrations_test.go - cabana/auth_test.go - cabana/commands.go - cabana/commands_test.go modified: - lagoon/backend_admin_migrations.go - cabana/auth.go - cabana/http.go - cabana/contracts.go - internal/build/build.go - ../fonoteka.go/main.go - ../fonoteka.go/config/admin.yaml key-decisions: - "Admin jti rows live in backend_jwt_blacklist, not the frontend jwt_blacklist, and cabana does not republish BlacklistStore" - "backend_user_roles.code stays nullable and non-unique, matching Winter, while name stays unique for the idempotent seed" - "tokens_valid_after is an extra nullable column so reset can revoke tokens without changing Winter's required columns" - "Login throttle defaults to 5 attempts per minute on the existing fixed-window limiter" patterns-established: - "Pattern: login always runs bcrypt, then rejects unknown, inactive, and bad-password with one body" - "Pattern: operator provisioning is cabana.RuntimeCommands appended by the app-main generator" requirements-completed: [AUTH-08] coverage: - id: D1 description: Backend identity tables, system-role seeds, indexes, and rollback match the Winter-shaped contract on PostgreSQL. requirement: AUTH-08 verification: - kind: integration ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminMigration status: pass - kind: integration ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminSeed status: pass - kind: integration ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminRollback status: pass - kind: integration ref: lagoon/backend_admin_migrations_test.go#TestBackendAdminWinterRow status: pass human_judgment: false - id: D2 description: Login, refresh, logout, and me issue and revoke backend-audience tokens, including inactive, deleted, stale, and blacklisted failures. requirement: AUTH-08 verification: - kind: integration ref: cabana/auth_test.go#TestAdminAuthLifecycle status: pass - kind: integration ref: cabana/auth_test.go#TestAdminInactive status: pass - kind: integration ref: cabana/auth_test.go#TestAdminDeleted status: pass - kind: integration ref: cabana/auth_test.go#TestAdminBlacklist status: pass - kind: integration ref: plugins/golem15/fonoteka/admin_auth_test.go#TestAdminAuthLifecycleAssembled status: pass human_judgment: false - id: D3 description: Repeated logins hit the fixed-window limiter, and auth logs keep outcome and admin id without passwords, hashes, tokens, or the signing secret. requirement: AUTH-08 verification: - kind: integration ref: cabana/auth_test.go#TestAdminLoginThrottle status: pass - kind: integration ref: cabana/auth_test.go#TestAdminAuthLogging status: pass human_judgment: false - id: D4 description: admin:create and admin:reset-password provision bcrypt admins, reject unknown or ambiguous roles, revoke old tokens, and are registered once in the generated binary. requirement: AUTH-08 verification: - kind: integration ref: cabana/commands_test.go#TestAdminCreateCommand status: pass - kind: integration ref: cabana/commands_test.go#TestAdminResetPasswordCommand status: pass - kind: unit ref: internal/build/build_test.go#TestGenerateMainRegistersCabanaRuntimeCommands status: pass - kind: unit ref: admin_command_test.go#TestAdminCommandRegistration status: pass human_judgment: false duration: 22min completed: 2026-09-24 status: complete plan_head_before: 0ed980e332239a32432f011686e0b2e6b1bd3573 plan_head_after: 5f218977e4cc61b103a3be4e459fe5aa6962006f --- # Phase 9 Plan 02: Backend identity lifecycle Summary **Backend admins now have a Winter-shaped PostgreSQL identity, a revocable backend-audience JWT lifecycle, and command-only provisioning on the generated binary.** ## Performance - **Duration:** 22 min - **Started:** 2026-09-24T15:35:45Z - **Completed:** 2026-09-24T15:57:36Z - **Tasks:** 3 - **Files modified:** 14 ## Accomplishments - Framework migrations create `backend_users`, `backend_user_roles`, and `backend_jwt_blacklist`, seed developer and publisher idempotently, and roll back without touching plugin history. - `POST /_admin/api/v1/auth/login`, `/refresh`, `/logout`, and `GET /me` use backend-audience JWTs, sliding refresh, opaque failures, a 5-per-minute login limiter, and logs that keep outcome and admin id only. - `admin:create` and `admin:reset-password` hash with bcrypt, validate role codes, revoke older tokens, and are appended once by the app-main generator. ## Task Commits Each task was committed atomically. SummerCMS `commits: 6` is `git rev-list --count` from the plan ledger. Fonoteka commits are in the sibling repository. 1. **Task 1: Exact backend identity migrations (RED)** - `448faa4` (test) 2. **Task 1: Exact backend identity migrations (GREEN)** - `06a7292` (feat) 3. **Task 2: Backend JWT lifecycle (RED)** - `0953308` (test, summercms.go) and `6349952` (test, fonoteka.go) 4. **Task 2: Backend JWT lifecycle (GREEN)** - `9740c3d` (feat, summercms.go) and `029f908` (feat, fonoteka.go) 5. **Task 3: Admin commands (RED)** - `d27f442` (test, summercms.go) and `9522c65` (test, fonoteka.go) 6. **Task 3: Admin commands (GREEN)** - `5f21897` (feat, summercms.go) and `e4d773d` (feat, fonoteka.go) **Plan metadata:** pending docs commit ## Files Created/Modified - `lagoon/backend_admin_migrations.go` - re-runnable identity DDL, system-role seed, and admin blacklist table - `lagoon/backend_admin_migrations_test.go` - real PostgreSQL column, seed, rollback, and Winter-row tests - `cabana/contracts.go` - GORM `BackendUser` and `BackendUserRole`, including the reset cutoff - `cabana/auth.go` - login, refresh, logout, me, safe logging, and the admin blacklist - `cabana/http.go` - mounts the auth routes and the login throttle - `cabana/commands.go` - `admin:create` and `admin:reset-password` - `internal/build/build.go` - generated main appends `cabana.RuntimeCommands` - `fonoteka.go` `main.go` - regenerated command registration - `fonoteka.go` `config/admin.yaml` - TTL, bcrypt cost, and login throttle defaults with an empty secret ## Decisions Made - Admin revocation uses its own `backend_jwt_blacklist` table. Cabana does not publish that store over the frontend `jwt_blacklist`. - `backend_user_roles.code` is indexed and not unique, so a copied Winter row can repeat a code. `admin:create --role` rejects zero or many matches. - `tokens_valid_after` is nullable and additive. Reset sets it one second ahead so existing backend JWTs fail the guard without changing Winter's required columns. - Login throttle defaults to 5 attempts per minute through `throttle:N,M` on the existing fixed-window limiter. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Lagoon tests no longer import cabana** - **Found during:** Task 3 (admin commands) - **Issue:** `cabana` must call `lagoon.OpenFromApp`, but `lagoon` tests imported `cabana.BackendUser`, which is an import cycle once that edge exists. - **Fix:** The Winter-row test loads a local GORM struct with the same column tags. Production `cabana.BackendUser` is unchanged. - **Files modified:** `lagoon/backend_admin_migrations_test.go` - **Verification:** `TestBackendAdminWinterRow` passed - **Committed in:** `5f21897` **2. [Rule 3 - Blocking] Regenerated main also restored `route:list`** - **Found during:** Task 3 (admin commands) - **Issue:** `internal/build/build.go` already emitted `surf.RouteListCommand`, but the tracked Fonoteka `main.go` had drifted and omitted it. - **Fix:** Regeneration followed the generator, so the tracked main gained that one existing line as well as `cabana.RuntimeCommands`. - **Files modified:** `fonoteka.go/main.go` - **Verification:** `TestAdminCommandRegistration` passed and `go test .` compiled the main package - **Committed in:** `e4d773d` --- **Total deviations:** 2 auto-fixed (2 blocking) **Impact on plan:** Both were required to keep the command path compiling and the generated binary equal to the generator. No new dependency and no production secret. ## TDD Gate Compliance | Gate | Commit | Result | |------|--------|--------| | RED task 1 | `448faa4` test(09-02) | `TestBackendAdminMigration` failed because `tokens_valid_after` and `backend_jwt_blacklist` were missing | | GREEN task 1 | `06a7292` feat(09-02) | migration, seed, rollback, and Winter-row tests passed on PostgreSQL | | RED task 2 | `0953308` / `6349952` test(09-02) | login left `last_login` null; logout was 404 | | GREEN task 2 | `9740c3d` / `029f908` feat(09-02) | lifecycle, throttle, logging, and assembled tests passed | | RED task 3 | `d27f442` / `9522c65` test(09-02) | `admin:create` was not registered and generated main lacked `cabana.RuntimeCommands` | | GREEN task 3 | `5f21897` / `e4d773d` feat(09-02) | create, reset, generator, and registration tests passed | `gsd_run check tdd-red-evidence` returned `RED_EVIDENCE_OK` for the migration, lifecycle, and create-command RED runs. The task 3 RED commit includes a nil `RuntimeCommands` stub so the Go tests compiled before the implementation replaced it. ## Authentication Gates None. ## Issues Encountered None. ## User Setup Required None - no external service configuration required. Production boots that register admin controllers must set `SUMMER_ADMIN__JWT__SECRET`. `config/admin.yaml` still ships that key empty. Login throttle, refresh TTL, grace, and bcrypt cost have non-secret defaults. ## Next Phase Readiness Ready for 09-03. Identity, revocation, and operator provisioning are in place. `AUTH-08` stays shared with 09-11 and 09-12, so it is not marked complete in REQUIREMENTS.md. ## Self-Check: PASSED - FOUND: lagoon/backend_admin_migrations.go, lagoon/backend_admin_migrations_test.go, cabana/commands.go, cabana/auth.go, cabana/http.go, cabana/contracts.go, internal/build/build.go - FOUND: fonoteka.go main.go, config/admin.yaml, plugins/golem15/fonoteka/admin_auth_test.go, admin_command_test.go - FOUND commits: 448faa4, 06a7292, 0953308, 6349952, 9740c3d, 029f908, d27f442, 9522c65, 5f21897, e4d773d --- *Phase: 09-backend-admin-authentication-and-schema-pipeline* *Completed: 2026-09-24*