The D-01 backend_users/backend_user_roles schema matches the locked Winter-shaped columns, seeds developer and publisher as system roles idempotently, and migrates up/down against real PostgreSQL without AutoMigrate.
Backend login, refresh, logout, and me use D-02 backend-audience JWTs, sliding refresh, and a PostgreSQL jti blacklist; inactive, soft-deleted, unknown, blacklisted, and stale principals fail closed.
D-04 `admin:create` and `admin:reset-password` operate through the generated app command surface, hash with the existing bcrypt helper, validate the selected role, and never echo a password or token.
Backend login is throttled with the existing fixed-window limiter, and successful, failed, and denied auth events are logged with outcome/admin ID only—never credentials or bearer tokens.
path
provides
lagoon/backend_admin_migrations_test.go
Real-PostgreSQL migration, seed, and rollback evidence
path
provides
cabana/commands.go
admin:create and admin:reset-password runtime commands
path
provides
cabana/auth_test.go
Complete backend token lifecycle and safe logging coverage
path
provides
../fonoteka.go/main.go
Generated binary command registration for cabana runtime commands
from
to
via
pattern
internal/build/build.go
cabana/commands.go
generated app main appends cabana.RuntimeCommands
cabana.RuntimeCommands
from
to
via
pattern
cabana/auth.go
bouncer/blacklist.go
backend refresh/logout use the PostgreSQL blacklist implementation
PostgresBlacklist
from
to
via
pattern
cabana/auth.go
lagoon/backend_admin_migrations.go
backend provider loads active non-deleted users and roles
backend_users
[FLAGGED-UNVERIFIED] First-admin provisioning must not occur at boot, through a web setup wizard, or from environment-seeded credentials.
[FLAGGED-UNVERIFIED] Admin authentication must not introduce a cookie session store or merge with the frontend user model.
[FLAGGED-UNVERIFIED] Authentication logs must not contain login passwords, JWTs, signing secrets, or password hashes.
Phase Goal
As a backend administrator, I want to authenticate separately and manage resources described by Winter-shaped schemas, so that the administration surface stays permission-gated and reusable without coupling it to frontend users.
Complete the backend identity lifecycle and operator provisioning surface established by the tracer.
Purpose: AUTH-08 requires a durable, independently operated admin identity—not just a test fixture capable of driving one route.
Output: Exact migrations with real-PostgreSQL proof, complete auth lifecycle, safe throttling/audit logging, and the two D-04 commands in generated binaries.
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-RESEARCH.md
@.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-01-SUMMARY.md
@lagoon/migrations.go
@lagoon/commands.go
@bouncer/blacklist.go
@bouncer/password.go
@internal/build/build.go
@../fonoteka.go/main.go
Use the 09-01 `cabana.BackendUser`, `cabana.BackendUserRole`, audience-aware bouncer functions, and activated registry. Preserve `lagoon.Migrate` ordering and generated-main construction while adding cabana commands as framework runtime commands.
Artifacts this phase produces
Complete cabana.BackendUser/BackendUserRole GORM contracts and backend UserProvider
cabana.RuntimeCommands, cabana.AdminCreateCommand, and cabana.AdminResetPasswordCommand
Login/refresh/logout/me handlers with PostgreSQL revocation and safe auth-event logging
Real-PostgreSQL migration/rollback tests and assembled auth lifecycle tests
Task 1: Finish exact backend identity migrations and persistence
D-01 intentionally matches cutover tables already named by Winter; changing columns later requires coordinated import/schema work, but the locked decision must not be re-gated.
lagoon/backend_admin_migrations.go, lagoon/backend_admin_migrations_test.go, cabana/contracts.go
lagoon/backend_admin_migrations.go, lagoon/migrations.go, lagoon/migrations_test.go, cabana/contracts.go, .planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-CONTEXT.md
- Test 1: fresh PostgreSQL migration creates the exact D-01 user/role columns, indexes, role relationship, timestamps and soft-delete behavior.
- Test 2: repeated migration leaves exactly one developer and one publisher system role with stable codes; rollback removes the framework admin tables without affecting plugin histories.
- Test 3: copied Winter-shaped rows load through the GORM contracts without a schema transform.
Complete the framework migration with explicit gormigrate Up/Down SQL and integrate it before plugin sets, alongside the existing framework-owned attachment migration. Model only D-01's backend users and roles; seed developer/publisher idempotently; preserve nullable role and last-login fields; and give the admin blacklist its own framework-owned table while reusing `bouncer.PostgresBlacklist`. Do not use AutoMigrate or add backend user groups/preferences/access-log tables. Prove exact columns, index/unique behavior, system-role idempotency, cutover-shaped row loading, and rollback on Testcontainers PostgreSQL.
go test ./lagoon -run '^TestBackendAdmin(Migration|Seed|Rollback|WinterRow)' -count=1
The command exits non-zero, reports no matching test, PostgreSQL is skipped, a locked column/index/role seed is missing, migration is non-idempotent, rollback damages another history, or AutoMigrate appears in production code.
The migration is an executable PostgreSQL contract for D-01 and preserves the existing per-framework/per-plugin migration ordering.
Backend identity persistence and system-role seeds round-trip on real PostgreSQL.
Task 2: Complete backend JWT lifecycle, throttle, and safe auth events
cabana/auth.go, cabana/http.go, cabana/auth_test.go, ../fonoteka.go/config/admin.yaml, ../fonoteka.go/plugins/golem15/fonoteka/admin_auth_test.go
cabana/auth.go, cabana/http.go, bouncer/refresh.go, bouncer/blacklist.go, bouncer/password.go, surf/limiter.go, ../fonoteka.go/plugins/golem15/fonoteka/admin_tracer_test.go, ../fonoteka.go/config/golem15.user.yaml
- Test 1: login by backend login or normalized email returns a backend-audience access token; refresh rotates and blacklists the previous jti; logout revokes it; me returns only safe backend profile/role data.
- Test 2: unknown, wrong-password, inactive, soft-deleted, stale, and blacklisted identities share opaque failures without timing-dependent record disclosure.
- Test 3: the existing fixed-window limiter rejects repeated login attempts, while captured logs record outcomes without password, bearer token, hash, or secret substrings.
Expand the tracer login into all D-09 auth routes using D-02's bouncer lifecycle and D-10 envelopes. Configure access/refresh TTL, grace, bcrypt cost, and a fail-loud backend secret under `admin.*`; attach the existing surf fixed-window limiter to login; update last_login only after successful password verification; require activated/non-deleted users on every token load; rotate/blacklist transactionally; and serialize a safe me DTO. Emit structured successful/failed/authorization-denied events containing outcome, stable admin ID when known, and normalized request metadata only. Reuse the shared test config helpers for the test-only secret so unrelated assembled-route tests remain honest.
go test ./cabana -run '^TestAdmin(AuthLifecycle|LoginThrottle|AuthLogging|Inactive|Deleted|Blacklist)' -count=1 && (cd ../fonoteka.go && go test ./plugins/golem15/fonoteka -run '^TestAdminAuthLifecycleAssembled$' -count=1)
Either command exits non-zero, reports no matching test, refresh/logout leaves an old token usable, inactive/deleted users authenticate, throttling does not reject the limit case, error bodies reveal account existence, or captured logs contain a credential/token/secret.
Login, refresh, logout, and me are fully assembled and every sensitive auth failure is both opaque to clients and redacted in logs.
The backend guard has a durable, revocable, separately configured session lifecycle.
Task 3: Provision and reset admins through generated framework commands
The command names and flags in D-04 are operator-facing contracts carried into generated app binaries.
cabana/commands.go, cabana/commands_test.go, internal/build/build.go, internal/build/build_test.go, ../fonoteka.go/main.go
lagoon/commands.go, bonfire/command.go, internal/build/build.go, internal/build/build_test.go, ../fonoteka.go/main.go, cabana/contracts.go
- Test 1: admin:create with email/password and optional login/superuser/role creates one activated bcrypt-backed admin, defaults login deterministically when omitted, and rejects unknown/ambiguous roles.
- Test 2: admin:reset-password accepts login or email, updates the bcrypt hash, invalidates earlier tokens, and reveals no password/hash.
- Test 3: a generated app main registers both commands exactly once and compiles.
Implement D-04 as cabana runtime bonfire commands that open/publish the database through existing lagoon helpers, validate flags and role code, use `bouncer.HashPassword`, and atomically create or update the backend row. On reset, advance the token-valid-after cutoff so existing admin JWTs stop working. Update the app-main generator to append cabana runtime commands and regenerate the tracked Fonoteka main through the established build path; do not hand-edit a command into only this app. Command output may identify the affected login/email but must never print a supplied password, stored hash, JWT, or signing secret.
go test ./cabana -run '^TestAdmin(Create|ResetPassword)Command' -count=1 && go test ./internal/build -run '^Test.*RuntimeCommands' -count=1 && (cd ../fonoteka.go && go test . -run '^Test.*AdminCommandRegistration' -count=1)
Any command exits non-zero, any package reports no matching test, generated main lacks or duplicates cabana commands, role validation is bypassed, old tokens survive reset, or command output contains password/hash/token material.
The operator can create the first admin and reset a password entirely through the generated binary using the exact D-04 command/flag surface.
Backend administrator provisioning is command-only, deterministic, bcrypt-backed, and token-revoking.
<threat_model>
Trust Boundaries
Boundary
Description
Operator CLI → backend tables
Sensitive password and role inputs create or mutate privileged identities.
Login/refresh/logout → token state
Untrusted credentials and bearer tokens cross into hashing, rotation, and revocation logic.
Auth outcomes → logs
Security telemetry must preserve evidence without copying secrets.
STRIDE Threat Register
Threat ID
Category
Component
Severity
Disposition
Mitigation Plan
T-09-03
Information Disclosure
cabana auth handlers and structured logging
high
mitigate
Opaque login errors plus explicit log-capture tests that reject passwords, hashes, JWTs, and secrets while retaining outcome/admin ID.
T-09-04
Elevation
admin:create/reset-password commands
medium
mitigate
Validate exact role codes, hash inside the transaction, invalidate tokens on reset, and test generated command registration/output redaction.
T-09-SC
Tampering
Go module dependency set
high
mitigate
Reuse existing bouncer/bonfire/lagoon/surf packages; no package installs are authorized, and any discovered dependency need halts for legitimacy review.
</threat_model>
- Real PostgreSQL migration up/down and seed idempotency tests pass.
- The assembled backend auth lifecycle covers every failure class and safe logging.
- Generated application command registration, create, and reset flows pass without exposing secrets.
<success_criteria>
Backend identity is durable, Winter-shaped, separately signed, refreshable, and revocable.
An operator can provision/reset admins without a web bootstrap path.
Authentication throttling and logging are executable security controls.
</success_criteria>
Create `.planning/phases/09-backend-admin-authentication-and-schema-pipeline/09-02-SUMMARY.md` when done.