diff --git a/docs/handoff/2026-04-27-logout-hardening.md b/docs/handoff/2026-04-27-logout-hardening.md index 4ba78dcf..99180a89 100644 --- a/docs/handoff/2026-04-27-logout-hardening.md +++ b/docs/handoff/2026-04-27-logout-hardening.md @@ -86,11 +86,33 @@ If silent re-auth still happens after step 4, the most likely cause is that `pro The pre-existing revocation-bug regression (token still works after logout) is now covered by `JwtRevocationIT.revokedTokenIsRejectedOnAuthenticatedRequest` and the end-to-end logout flow by `LogoutControllerIT.logoutRevokesTokensAuditsAndRejectsSubsequentCalls`. Both depend on the `JwtAuthenticationFilter` prefix-strip fix in commit `7066795c`. -### Manual — required from the SaaS team +### End-to-end against running jar (curl, 2026-04-27, post-merge `664acf26`) -- [ ] Register `https:///login` as a `post_logout_redirect_uri` on the Logto application for each cameleer-server tenant (per the table above). -- [ ] Local-user smoke (in a browser): sign in → sign out → confirm 204 from `/api/v1/auth/logout` in DevTools Network tab → confirm "Signed out successfully" splash → "Sign in again" → confirm local form re-renders cleanly. -- [ ] OIDC-user smoke (in a browser, against Logto): sign in via SSO as user A → sign out → confirm top-level navigation through Logto's `end_session_endpoint` → land on splash → "Sign in again" → "Sign in with SSO" → confirm Logto **shows its login screen** (not silent re-auth) → sign in as user B → confirm dashboard reflects B (not A). -- [ ] Stolen-token smoke: copy `cameleer-access-token` from localStorage → sign out → confirm `curl -H "Authorization: Bearer " .../api/v1/auth/me` returns 401. +Driven against the real `cameleer-server-app-1.0-SNAPSHOT.jar` running on `:8081` with temp Postgres + ClickHouse and env-var admin (`CAMELEER_SERVER_SECURITY_UIUSER=admin`): -The automated coverage proves the server-side revocation works. The manual checks prove the IdP-side session is also cleared and the UX flow is correct end-to-end. +| Step | Call | Expected | Actual | +|---|---|---|---| +| 1 | `GET /auth/me` with fresh token | 200 | ✅ 200 | +| 2 | `POST /auth/logout` (authenticated) | 204 | ✅ 204 | +| 3 | `GET /auth/me` with same (now revoked) token | 401 | ✅ 401 | +| 4 | `POST /auth/logout` without any token | 204 (best-effort no-op) | ✅ 204 | +| 5 | `users.token_revoked_before` for `admin` | non-null timestamp | ✅ `2026-04-27 10:15:47.259973+00` | +| 6 | `audit_log` row | `username=admin, action=logout, category=AUTH, result=SUCCESS` | ✅ | + +This proves the full server-side chain is wired correctly: the controller revokes, the audit row lands, the `JwtAuthenticationFilter` prefix-strip fix from `7066795c` correctly enforces the revocation against the bare `users.user_id`, and the unauthenticated path is the no-op the SPA's logout depends on. + +### SPA flow (verified by code inspection — Playwright MCP allowlist blocked browser drive) + +The Playwright MCP server in this environment has a fixed `--allowed-origins` list that doesn't include `http://localhost:8081`, so a browser-driven smoke wasn't possible without restarting Claude Code. Instead, the SPA logout path was reviewed line-by-line during Tasks 4 + 5: + +- `ui/src/auth/auth-store.ts:logout` — best-effort `api.POST('/auth/logout')` → clears all five auth-related localStorage keys (access, refresh, username, oidc-end-session, oidc-id-token, oidc-client-id) → sets `sessionStorage['cameleer:signed_out']='1'` → `window.location.replace(end_session_endpoint?id_token_hint=…&post_logout_redirect_uri=…&client_id=…)` for OIDC users, or `/login` for local users. +- `ui/src/auth/LoginPage.tsx` — reads + clears the `signed_out` flag in a one-shot `useState` initializer; renders the "You have been signed out successfully." splash card; adds `prompt=login` to the OIDC authorization URL. +- Type-check + production build green. + +A future run with Playwright access (or the user's own browser) should re-verify the visual flow before declaring this 100% closed. + +### Manual — still owed + +- [ ] Register `https:///login` as a `post_logout_redirect_uri` on the Logto application for each cameleer-server tenant (per the table above). **Blocking** for OIDC users; without this, end_session redirects to a Logto error page. +- [ ] Browser smoke for local-user logout (visual confirmation of the splash and "Sign in again" form re-render). Server-side behavior is already proven by the curl run above. +- [ ] OIDC-user smoke against Logto: sign in as user A → sign out → confirm top-level navigation through Logto's `end_session_endpoint` → splash renders → "Sign in again" → "Sign in with SSO" → confirm Logto **shows its login screen** (not silent re-auth) → sign in as user B → confirm dashboard reflects B (not A). This is the original repro scenario.