Files
admin/_docs/02_tasks/done/AZ-535_logout_revocation.md
Oleksandr Bezdieniezhnykh 8e7c602f51
ci/woodpecker/push/01-test Pipeline failed
ci/woodpecker/push/02-build-push unknown status
[AZ-535] [AZ-533] Logout/revocation surface + UAV mission tokens
AZ-535: POST /logout (caller's session), /logout/all (all sessions for user),
admin POST /sessions/{sid}/revoke, and verifier-only GET /sessions/revoked
snapshot. New Service role gates the snapshot. Idempotent revoke; reason +
revoked_by_user_id audited per row.

AZ-533: POST /sessions/mission mints a long-lived no-refresh ES256 token bound
to one aircraft + one mission. Audience narrowed to satellite-provider, hard
12 h cap, persisted as class='mission' so the existing logout/revoke surface
covers it. Successful CompanionPC /login or /token/refresh auto-revokes that
aircraft's open mission session (post-flight reconnect).

Schema: 09_sessions_logout_and_mission.sql adds revoked_by_user_id, class,
aircraft_id; drops NOT NULL on refresh_hash for mission rows; adds two partial
indexes for the auto-revoke and snapshot hot paths.

Tests: 13 new e2e tests, all green; full suite 75/76 (1 pre-existing flake in
PasswordHashingTests AC5 timing assertion, unrelated to this batch).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 05:51:23 +03:00

5.1 KiB

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=<ts> 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=<unix-ts> 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=<ts> 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.