# Logout Endpoint + Revocation Surface for Verifiers **Task**: AZ-535_logout_revocation **Name**: Logout endpoint + revocation surface for verifiers **Description**: Add `POST /logout`, `POST /logout/all`, admin-only `POST /sessions/{sid}/revoke`, and a `GET /sessions/revoked?since=` snapshot endpoint that verifiers (satellite-provider, gps-denied, ui) poll to maintain a local denylist. Without this, JWTs cannot be revoked before `exp`. **Complexity**: 3 points **Dependencies**: AZ-531 (needs the `sessions` table); coordinate `jti`/`sid` claim stamping **Component**: Admin API + Services + DataAccess **Tracker**: AZ-535 **Epic**: AZ-529 ## Problem With stateless JWT validation, logout doesn't actually exist. Calling `/logout` on admin can clear admin's session, but satellite-provider, gps-denied, and any other verifier keep accepting the same token until `exp`. There is no way to forcibly kick a session in real time (e.g. "GPS permission revoked, end the flight"). ## Outcome - `POST /logout` endpoint: revokes the caller's current session (refresh + all access tokens minted from it). Idempotent. - `POST /logout/all` endpoint: revokes every session for the caller's user (full "sign out everywhere"). - `POST /sessions/{sid}/revoke` (admin-only): revoke any session by id ("GPS permission revoked, kill flight UAV-117 mission M-042"). - Verifiers consume revocation via either: - **Pull mode (default)**: `GET /sessions/revoked?since=` returns `[{ jti, sid, exp }]`. Verifiers poll every 30 s and maintain a local denylist with TTL = token's remaining lifetime. - **Push mode (optional)**: a Redis pub/sub channel `auth:revoked` for sub-second propagation. Pull is mandatory; push is best-effort acceleration. ## Scope ### Included - `POST /logout`, `POST /logout/all`, `POST /sessions/{sid}/revoke` handlers in `Azaion.AdminApi/Program.cs`. - `GET /sessions/revoked?since=` endpoint authenticated via service-to-service JWT issued to each verifier identity (each verifier has a dedicated `Role=Service` user). - Update `sessions` table with `revoked_at`, `revoked_reason`, `revoked_by_user_id` (these columns may already be present from AZ-531; if so, this ticket only adds `revoked_by_user_id`). - Snapshot endpoint must auto-prune entries whose `exp < now()` so the response stays bounded. - Tests: logout works, all-logout works, admin-revoke works, revoked endpoint returns recent revocations and excludes expired. ### Excluded - Verifier-side denylist consumption (per-verifier ticket, filed when admin ships). - Redis pub/sub push channel — nice-to-have; pull-based snapshot is the contract. - Per-permission revocation in real time (e.g. "revoke just GPS, keep session alive") — architecturally requires moving permissions out of the JWT; future ticket. ## Acceptance Criteria **AC-1: /logout revokes the session** Given a valid access + refresh token pair When `POST /logout` is called with the access token Then the session row is marked `revoked_at=now()`, `revoked_reason='user_logout'`. The refresh token stops working. **AC-2: /logout/all revokes every session for the user** Given user U has 3 active sessions When `POST /logout/all` is called from any one of them Then all 3 sessions are revoked. **AC-3: Admin can revoke any session by id** Given user U has session SID-X When an Admin-role JWT calls `POST /sessions/SID-X/revoke` Then SID-X is marked revoked with `revoked_by_user_id` = the admin's id. **AC-4: /sessions/revoked snapshot returns recent revocations** Given 5 sessions revoked in the last hour, 2 of which already expired When `GET /sessions/revoked?since=<1h-ago>` is called by an authenticated verifier Then response is the 3 non-expired ones, with `[{ jti, sid, exp }]`. `Cache-Control: no-cache` (this is real-time data). **AC-5: Idempotent logout** Given a session already revoked When `POST /logout` is called again with the same token Then 200 with `{ already_revoked: true }`. No DB write. ## Blackbox Tests | AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References | |--------|------------------------|-------------|-------------------|----------------| | AC-1 | Active session | POST /logout | Session revoked, refresh dead | — | | AC-2 | User with 3 sessions | POST /logout/all from any | All 3 revoked | — | | AC-3 | Admin JWT, target SID-X | POST /sessions/SID-X/revoke | SID-X revoked with admin id | — | | AC-4 | 5 revoked (2 expired) | GET /sessions/revoked?since=… | Returns 3 non-expired | — | | AC-5 | Already-revoked session | POST /logout again | 200 already_revoked, no DB write | — | ## Risks / Notes - The pull endpoint must NOT leak revocations across users to non-admin callers. Verifier identity is service-level (each verifier has a dedicated `Role=Service` user with read-revocations permission); they get the global feed. Regular users only see their own sessions if a future endpoint is added. - 30 s polling means up to 30 s of "stale token works" after logout. Documented as acceptable; for sub-second, deploy the optional Redis push. - Coordinate auto-prune cadence to keep snapshot < 5 KB even at high revocation rates.