--- phase: 07-user-plugin-and-authentication plan: 02 subsystem: auth tags: [user, jwt, throttle, register, festival] requires: - phase: 07-user-plugin-and-authentication provides: bouncer Mint, Refresh, BlacklistStore, bcrypt, lagoon email/confirmed provides: - golem15.user login, logout, fetch, refresh, register, oauth-providers - user_throttle and jwt_blacklist migrations - GetApiArrayEvent collected by golem15.fonoteka affects: [07-03, 07-04, 07-05] tech-stack: added: [] patterns: [Bearer-only session handlers, cookie fallback only on the registry jwt guard, pre-password throttle gate] key-files: created: - ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go - ../fonoteka.go/plugins/golem15/user/routes.go - ../fonoteka.go/plugins/golem15/user/classes/events.go - ../fonoteka.go/plugins/golem15/user/classes/throttle.go modified: - ../fonoteka.go/plugins/golem15/user/plugin.go - ../fonoteka.go/plugins/golem15/fonoteka/plugin.go key-decisions: - "Login gates with RejectIfThrottled before the password check and records the attempt once afterward" - "Disabled and throttled registration return the production SafeExceptionResponse body unless app.debug is true" - "Fonoteka listens for GetApiArrayEvent; the user plugin does not import fonoteka" patterns-established: - "Pattern: /_user/api/v1 group middleware is only throttle:user-api; handlers authenticate Bearer-only" - "Pattern: mail template names go through MailTemplate and ResolveMailLocale" requirements-completed: [] duration: 83min completed: 2026-09-22 --- # Phase 7 Plan 02: User session loop Summary **Login, logout, fetch, refresh, and register on `/_user/api/v1`, with a per-user login throttle, a Postgres JWT blacklist, and fonoteka's organisation fields merged through `GetApiArrayEvent`.** ## Performance - **Duration:** 83 min - **Started:** 2026-09-22T11:48:00Z - **Completed:** 2026-09-22T13:11:00Z - **Tasks:** 3 - **Files modified:** 22 ## Accomplishments - Email and password login returns a JWT whose `sub` is the user id, `prv` is the hardcoded User hash, and `iss` is the request URL. Logout forever-blacklists that jti so the next fetch is 401. - Wrong password, an unknown email, and a suspended account share the body `{"error":true,"message":"Invalid email or password"}`. - Register covers auto (token), user (activation mail), and admin (`{}`). Production hides disabled and throttled causes behind `{"error":"Internal server error"}`. - `golem15.fonoteka` adds `organisation_id`, `organisation_role`, `must_change_password`, and `preferred_locale` on `GetApiArrayEvent`. ## Task Commits 1. **Task 1: User session schema and config** — `7046213` (test), `4cc7433` (feat) in `fonoteka.go` 2. **Task 2: Throttle, mail locale, login/logout/fetch/refresh** — `1076db9`, `1dd4aba` in `fonoteka.go` 3. **Task 3: Register and GetApiArrayEvent** — `bac71fe` in `fonoteka.go` ## Files Created/Modified - `plugins/golem15/user/controllers/api_controller.go` — session and register handlers - `plugins/golem15/user/routes.go` — `/_user/api/v1` group with `throttle:user-api` - `plugins/golem15/user/plugin.go` — Postgres blacklist, cookie-capable jwt guard, user-api bucket, sweep - `plugins/golem15/user/classes/throttle.go` — failed-login counter and the pre-password gate - `plugins/golem15/user/classes/events.go` — `GetApiArrayEvent` and `RegisterEvent` - `plugins/golem15/fonoteka/plugin.go` — payload listener ## Decisions Made The login gate does not increment the attempt counter. `CheckAndRecordLogin(..., false)` both checks a ban and records a failure, so calling it before and after the password check would suspend on the third HTTP failure. `RejectIfThrottled` only reports an existing ban or suspension. The outcome is recorded once. Registration's disabled and throttled branches use `{"error":"..."}` at 500. That is distinct from `wire.WriteOpaque500`, which is `{"error":true,"message":"Internal server error"}`. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] The pre-password throttle call must not increment attempts** - **Found during:** Task 2 - **Issue:** The plan called `CheckAndRecordLogin(..., false)` before the password check and again on failure. That function increments on `ok=false`, so each failed login counted twice and the sixth HTTP login was not the one rejected before the password comparison. - **Fix:** `RejectIfThrottled` enforces ban and suspension without writing. `CheckAndRecordLogin` runs once with the outcome. - **Files modified:** `classes/throttle.go`, `controllers/api_controller.go` - **Verification:** `TestLoginSixthAttempt` — five failures suspend, the sixth correct password returns the generic 401, and `attempts` stays 5. - **Committed in:** `1dd4aba` --- **Total deviations:** 1 auto-fixed (Rule 1) **Impact on plan:** Keeps the 5-attempt / 15-minute suspend aligned with one failed HTTP login. No new route. ## Issues Encountered None ## User Setup Required None - no external service configuration required. ## Next Phase Readiness Ready for 07-03 (account management) and 07-04 (personal tokens). AUTH-01 stays open: password reset and email verification are still 07-03. AUTH-02's payload seam is in place; the requirement checkbox stays pending until the phase requirement is signed off. ## Self-Check: PASSED - Session and register handlers exist under `plugins/golem15/user/controllers/api_controller.go`. - `fonoteka.go` commits `7046213`, `4cc7433`, `1076db9`, `1dd4aba`, and `bac71fe` are on master. - `go test` for the session, throttle, register, and `TestGetApiArray` filters passed. `go vet` on the user and fonoteka plugins passed.