docs(handoff): logout-hardening — server-side end-to-end verified
Drove the full revocation flow against a running cameleer-server-app jar (temp postgres+clickhouse, env-var admin): GET /auth/me with fresh token -> 200 POST /auth/logout -> 204 GET /auth/me with same revoked token -> 401 POST /auth/logout (unauthenticated) -> 204 users.token_revoked_before -> non-null audit_log (action=logout, category=AUTH) -> 1 row, SUCCESS Proves the full chain end-to-end: controller revokes, audit lands, and the JwtAuthenticationFilter prefix-strip fix actually enforces revocation against the bare users.user_id (the original bug). Browser-driven SPA smoke is still owed — Playwright MCP allowlist in this env blocks 8081, so the SPA flow was verified by code-inspection during Tasks 4+5. OIDC-user smoke against Logto remains owed pending post_logout_redirect_uri registration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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://<tenant-base-url>/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 <token>" .../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://<tenant-base-url>/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.
|
||||
|
||||
Reference in New Issue
Block a user