diff --git a/.planning/notes/2026-10-22-external-user-plugin-escalations.md b/.planning/notes/2026-10-22-external-user-plugin-escalations.md index 5d0fd398..d4dffce8 100644 --- a/.planning/notes/2026-10-22-external-user-plugin-escalations.md +++ b/.planning/notes/2026-10-22-external-user-plugin-escalations.md @@ -39,11 +39,11 @@ Status values: `open` · `done` · `declined`. Shipped: local commit now; replac /msd-quick --validate /fetch /refresh /update /change-password must accept the token/auth_token cookies the same way jwt.auth and EndSession already do. Logout is already fixed. See .planning/todos/pending/2026-10-22-user-session-routes-ignore-cookie.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md -/msd-quick --discuss --validate Under serve, user-plugin Boot runs before lagoon.Publish so the jwt blacklist is never wired and logout does not revoke. Wire BlacklistStore and RegisterHooks through lagoon.OnDatabase; test Activate-then-Publish. See .planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md +/msd-quick --discuss --validate Under serve, user-plugin Boot runs before lagoon.Publish so the jwt blacklist is never wired and logout does not revoke. Wire BlacklistStore and RegisterHooks through lagoon.OnDatabase; test Activate-then-Publish. When re-registering the jwt guard, pass classes.SessionCookieNames() instead of the hardcoded token/auth_token names (G3 leftover). See .planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md /msd-quick --validate User-plugin /login throttle must use surf.ClientIP (trusted proxies), not RemoteAddr. Remaining tokens_valid_after writers (reset-password, admin password set) must Truncate(time.Second) like RevokeUserAccess so a same-second JWT iat is not refused. See .planning/todos/pending/2026-10-22-user-login-lockout-and-same-second-cutoff.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md -/msd-quick --discuss --validate Opt-in HttpOnly session cookie on user-plugin POST /login, following sociallogin token_cookie / bouncer.LoadCookieConfig. Default off so existing body-token clients stay unchanged. See .planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md +/msd-quick --discuss --validate Opt-in HttpOnly session cookie on user-plugin POST /login, following sociallogin token_cookie / bouncer.LoadCookieConfig. Default off. Discuss: a cookie-carried /refresh must rotate that cookie (G3 left body-only); keep SameSite=Lax, no new CSRF token. See .planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md /msd-quick --validate User-plugin config switch that does not register POST /login /forgot-password /reset-password so they 404. Default keeps today’s routes. No framework route-wrap seam. See .planning/todos/pending/2026-10-22-user-password-routes-404-switch.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md ``` @@ -53,12 +53,12 @@ Suggested order if you care about risk: G4 (serve blacklist), then G3, G2, 9+10, ## Verdicts (kept for the sessions) - **G2.** `require_activation` is documented and `/register` honors it; `/login` does not. Deactivation (`deleted_at`) stays reversible. -- **G3.** Logout already reads cookies (`358ee5d`). `/fetch`, `/refresh`, `/update`, `/change-password` still use `bearerToken()` / a cookie-less guard. -- **G4.** `Boot` before `lagoon.Publish`. Tests publish first and hide it. Seam is `lagoon.OnDatabase`. `RegisterHooks` has the same hole. +- **G3.** Done locally (`8e4b863`). Cookie extractors are `classes.SessionToken` / `SessionCookieNames`. `/refresh` does not rewrite a cookie. CSRF was not added (SameSite=Lax + current-password on change-password). +- **G4.** `Boot` before `lagoon.Publish`. Tests publish first and hide it. Seam is `lagoon.OnDatabase`. `RegisterHooks` has the same hole. When re-registering the jwt guard, pass `classes.SessionCookieNames()`. - **G6.** D-27: email change clears `email_verified_at` and does not write a new proof. A confirm-the-new-inbox flow would be a later feature. - **G7.** Stock form omission was a 12.1 decision. Stale `organisation_role` is already cleared (`ClearOnChange`, v0.1.8). Apps extend the form. - **G8.** Personal tokens die via `revoked_at`. `RevokeUserAccess` already sets that on deactivate/ban. Do not teach the API-token guard a JWT cutoff. Password reset killing API tokens would be a separate, explicit revoke. -- **7.** Sociallogin already has `token_cookie` for OAuth. Password `/login` does not set a cookie. +- **7.** Sociallogin already has `token_cookie` for OAuth. Password `/login` does not set a cookie. `--discuss` must decide cookie rotation on `/refresh` (G3 left the body-only rotate) and keep SameSite=Lax (no new CSRF token). - **8.** Plugin config; no framework wrap. - **9.** `/login` uses `RemoteAddr`; `/register` already uses `surf.ClientIP`. - **10.** `RevokeUserAccess` already truncates. Reset-password and the admin password set still stamp `time.Now()`. diff --git a/.planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md b/.planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md index b421d653..8105af9d 100644 --- a/.planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md +++ b/.planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md @@ -26,6 +26,8 @@ Phase 19.6 (2FA) is not mid-flight; `Login()` is free to change. Opt-in config on the user plugin (decide the key in `--discuss`: reuse a shared `auth.cookie` / bouncer cookie config, or a `golem15.user.jwt.token_cookie` twin of sociallogin). Default off, body still carries `token` unless the discuss decision matches sociallogin’s “cookie set, token omitted”. Cookie attributes from `bouncer.LoadCookieConfig`. No cookie when the flag is off (current contract). +`--discuss` also locks: (1) a cookie-carried `/refresh` must rewrite the session cookie (G3 left body-only rotate, which is unusable for an HttpOnly cookie the browser cannot store); follow `bouncer.CookieRefreshHandler`. (2) keep SameSite=Lax and do not add a CSRF token — that is the existing `jwt.auth` POST story (`/tokens`, `/logout`). Do not switch the cookie to SameSite=None. + After the commit, fill item 7 in `.planning/notes/2026-10-22-external-user-plugin-escalations.md`. ## Note from G3 (2026-10-22) @@ -35,5 +37,5 @@ Since sm-user-plugin `8e4b863`, `POST /refresh` accepts the `token` and `auth_to ## Invoke ``` -/msd-quick --discuss --validate Opt-in HttpOnly session cookie on user-plugin POST /login, following sociallogin token_cookie / bouncer.LoadCookieConfig. Default off so existing body-token clients stay unchanged. See .planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md +/msd-quick --discuss --validate Opt-in HttpOnly session cookie on user-plugin POST /login, following sociallogin token_cookie / bouncer.LoadCookieConfig. Default off. Discuss: a cookie-carried /refresh must rotate that cookie (G3 left body-only); keep SameSite=Lax, no new CSRF token. See .planning/todos/pending/2026-10-22-user-login-opt-in-session-cookie.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md ``` diff --git a/.planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md b/.planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md index 8cf4621c..82e4fc57 100644 --- a/.planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md +++ b/.planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md @@ -24,10 +24,14 @@ Confirmed. `party.Activate` (and every plugin `Boot`) runs in generated `main` b Use `lagoon.OnDatabase` from `Boot` (the documented seam: lighthouse and beachcomber already do). On publish: install `RegisterHooks`, publish `bouncer.NewPostgresBlacklist(sqlDB, "jwt_blacklist")`, and give the jwt guard that store (re-register, or a lazy store that `Lookup`s at request time — decide in `--discuss`). A test must boot like `serve`: Activate/Boot, then Publish, then login + logout + jwt.auth 401 and a blacklist row. A test that publishes first is not enough. -After the commit, fill G4 in `.planning/notes/2026-10-22-external-user-plugin-escalations.md`. +**G3 leftover:** `plugin.go` still registers the jwt guard with the cookie names written out (`"token", "auth_token"`). G3 exported `classes.SessionCookieNames()` and the controllers already pass it. When this quick reworks that `Register` line, switch to `classes.SessionCookieNames()...` so the guard and `SessionToken` cannot drift. Until then `TestSessionRoutesAcceptCookies` is the drift net. + +Base is sm-user-plugin `8e4b863` (G3). Hold push/tag for SummerCMS v0.5.0. + +After the commit, fill G4 in `.planning/notes/2026-10-22-external-user-plugin-escalations.md` the same way as G2/G3 (`done (unreleased)`, ships in v0.5.0). Leave the todo in pending with an Outcome. ## Invoke ``` -/msd-quick --discuss --validate Under serve, user-plugin Boot runs before lagoon.Publish so the jwt blacklist is never wired and logout does not revoke. Wire BlacklistStore and RegisterHooks through lagoon.OnDatabase; test Activate-then-Publish. See .planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md +/msd-quick --discuss --validate Under serve, user-plugin Boot runs before lagoon.Publish so the jwt blacklist is never wired and logout does not revoke. Wire BlacklistStore and RegisterHooks through lagoon.OnDatabase; test Activate-then-Publish. When re-registering the jwt guard, pass classes.SessionCookieNames() instead of the hardcoded token/auth_token names (G3 leftover). See .planning/todos/pending/2026-10-22-user-logout-blacklist-missing-under-serve.md and .planning/notes/2026-10-22-external-user-plugin-escalations.md ```