From 6a1974c0d2ee89ead533ee8f887d7be2f7549f10 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Thu, 22 Oct 2026 23:45:20 +0200 Subject: [PATCH] docs: fill item 7 in the external-escalation reply --- .../2026-10-22-external-user-plugin-escalations.md | 10 +++++----- .../2026-10-22-user-login-opt-in-session-cookie.md | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) 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 8ef0476e..c237a223 100644 --- a/.planning/notes/2026-10-22-external-user-plugin-escalations.md +++ b/.planning/notes/2026-10-22-external-user-plugin-escalations.md @@ -25,7 +25,7 @@ Reviewed 2026-10-22. The list is `golem15.user` (sm-user-plugin), not cabana adm | 4 | G6 `/update` email without proof | Decline | — | — | declined | n/a | | 5 | G7 Users form has no role field | Decline | — | — | declined | n/a | | 6 | G8 API token guard ignores `tokens_valid_after` | Decline | — | — | declined | n/a | -| 7 | Cookie option on `/login` | Feature | [2026-10-22-user-login-opt-in-session-cookie.md](../todos/pending/2026-10-22-user-login-opt-in-session-cookie.md) | strnsccy | open | | +| 7 | Cookie option on `/login` | Feature | [261022-wah](../quick/261022-wah-opt-in-httponly-session-cookie-on-user-p/) · [todo](../todos/pending/2026-10-22-user-login-opt-in-session-cookie.md) | strnsccy | done (unreleased) | `846087d`, `8bdf2c7` → v0.5.0 | | 8 | Password-routes 404 switch | Feature | [2026-10-22-user-password-routes-404-switch.md](../todos/pending/2026-10-22-user-password-routes-404-switch.md) | h7yrafgd | open | | | 9 | Lockout keyed on `RemoteAddr` | Fix | [261022-vfz](../quick/261022-vfz-user-plugin-login-throttle-via-surf-clie/) · [todo](../todos/pending/2026-10-22-user-login-lockout-and-same-second-cutoff.md) | sniewiif | done (unreleased) | `b6f8055` → v0.5.0 | | 10 | Same-second token refusal | Fix | [261022-vfz](../quick/261022-vfz-user-plugin-login-throttle-via-surf-clie/) · [todo](../todos/pending/2026-10-22-user-login-lockout-and-same-second-cutoff.md) | sniewiif | done (unreleased) | `a484d0e` → v0.5.0 | @@ -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.** 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). +- **G3.** Done locally (`8e4b863`). Cookie extractors are `classes.SessionToken` / `SessionCookieNames`. Item 7 (`846087d`) made a `/refresh` carried by the `auth.cookie` cookie rewrite it; bearer- and `auth_token`-carried refreshes stay body-only. CSRF was not added (SameSite=Lax + current-password on change-password). - **G4.** Done locally (`4bf44fc`). Lazy `appBlacklist` + `lagoon.OnDatabase` for hooks, Postgres store and sweep. Guard uses `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. Next quick: `golem15.user.jwt.token_cookie` twin (default off); when on, omit body `token` like sociallogin `writeSession`; cookie-carried `/refresh` must rotate (G3 leftover); SameSite=Lax, no CSRF. +- **7.** Done locally (`846087d`, `8bdf2c7`). `846087d`: a `/refresh` carried by the `auth.cookie` cookie (`token` by default, no bearer) rewrites it and answers `{}`, whatever `token_cookie` says; bearer and other-cookie refreshes stay body-only; refusals write no cookie. `8bdf2c7`: `golem15.user.jwt.token_cookie` (default off) makes /login, /register (auto sign-in) and /activate-by-code set the HttpOnly `auth.cookie` cookie via `bouncer.LoadCookieConfig` and omit body `token`; off keeps today's bodies. SameSite=Lax, no CSRF; API tokens untouched. - **8.** Plugin config; no framework wrap. - **9.** Done locally (`b6f8055`). /login keys the throttle on surf.ClientIP with http.trusted_proxies (the /register expression); RemoteIP stays the RemoteAddr helper; without trusted proxies X-Forwarded-For is ignored. - **10.** Done locally (`a484d0e`). /reset-password and the admin password set truncate the cutoff to whole seconds like RevokeUserAccess; change-password and bouncer unchanged. @@ -76,7 +76,7 @@ Ships in SummerCMS v0.5.0 (sm-user-plugin 15ff979, tagged with that wave): with G3 — some routes ignore the cookie Partly already fixed, rest agreed. POST /logout already accepts the token/auth_token cookies (and blacklists every JWT the request carries). GET /fetch, POST /refresh, POST /update and POST /change-password still required the Authorization header. -Ships in SummerCMS v0.5.0 (sm-user-plugin 8e4b863, tagged with that wave): GET /fetch, POST /refresh, POST /update and POST /change-password (and the other signed-in routes: /activate, /marketing-consent, /avatar, /avatar/remove) read the session token like jwt.auth: the Authorization header first, then the token and auth_token cookies. A cookie-carried /refresh returns the rotated token in the body and does not rewrite the cookie; the previous token is revoked, so store the new one in your cookie. +Ships in SummerCMS v0.5.0 (sm-user-plugin 8e4b863, tagged with that wave): GET /fetch, POST /refresh, POST /update and POST /change-password (and the other signed-in routes: /activate, /marketing-consent, /avatar, /avatar/remove) read the session token like jwt.auth: the Authorization header first, then the token and auth_token cookies. A /refresh carried by the session cookie named in auth.cookie (token by default) rewrites that cookie with the rotated token and answers {} (see "Cookie on /login" below). A /refresh carried by the Authorization header, or by the auth_token cookie, returns the rotated token in the body and writes no cookie; the previous token is revoked, so store the new one yourself. G4 — logout does not blacklist under serve Agreed. Plugin Boot runs before serve publishes the database, so the in-process blacklist store was never wired in production (the jwt_blacklist table exists; nothing wrote to it). Tests hid this because they publish the DB before Boot. @@ -93,7 +93,7 @@ As designed. Personal API tokens are revoked through revoked_at, not the JWT cut Cookie on /login Agreed as an opt-in. Sociallogin already has token_cookie for OAuth complete; password /login will grow the same kind of flag (default off, so body-token clients stay unchanged). -TODO: ships in SummerCMS v0.5.0 — config key , HttpOnly cookie via bouncer.LoadCookieConfig. +Ships in SummerCMS v0.5.0 (sm-user-plugin 846087d and 8bdf2c7, tagged with that wave): set golem15.user.jwt.token_cookie: true (default false) and POST /login, POST /register (when it signs the new account in) and POST /activate-by-code set the HttpOnly session cookie with the auth.cookie attributes (bouncer.LoadCookieConfig: name token, path /, SameSite=Lax, Secure from app.url, Max-Age the refresh window) and leave token out of the body; user (and message) stay. A POST /refresh carried by that cookie rewrites it with the rotated token and answers {}; a bearer refresh is unchanged. With the key off nothing changes. SameSite stays Lax and there is no CSRF token. Keep auth.cookie.name at token or auth_token, the cookies the session routes read. Password-routes switch Agreed, in the user plugin, not as a framework “wrap another plugin’s routes” seam. A config key will stop registering /login, /forgot-password and /reset-password so they 404. 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 cfe0bbdb..188efd93 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 @@ -39,6 +39,10 @@ Base is sm-user-plugin `a484d0e` (items 9+10). Hold push/tag for SummerCMS v0.5. Since sm-user-plugin `8e4b863`, `POST /refresh` accepts the `token` and `auth_token` cookies, answers the rotated token in the body only and writes no cookie. Cookie rotation is no longer optional: when the request arrived on a session cookie, rewrite it. +## Outcome (2026-10-22) + +Quick `261022-wah`. Two commits on sm-user-plugin master, `846087d` then `8bdf2c7` (base `a484d0e`). `846087d`: `POST /refresh` rewrites the `auth.cookie` cookie (`auth.cookie.name`, default `token`) when that cookie carried the token without a bearer header, answers `{}`, and keeps bearer and `auth_token` refreshes body-only; refusals write no cookie. The new export `classes.SessionTokenCookie` names the carrying cookie, sharing one bearer parser with `SessionToken`. `TestRefreshRewritesAuthCookie` (Docker) and `TestSessionTokenCookie` (`-short`) cover it. `8bdf2c7`: `golem15.user.jwt.token_cookie` (default off) on `/login`, `/register` (auto sign-in) and `/activate-by-code` through `writeSession`, the user plugin's twin of sociallogin's. The `auth.cookie` config is loaded right after `sessionDeps`, before any side effect, so a broken block answers the opaque 500 without signing in, creating or activating anything. README and `config.yaml` updated; `TestTokenCookie` covers it. Reply note item 7 and the G3 refresh sentence filled. Not tagged, not pushed: ships with the v0.5.0 wave. + ## Invoke ```