- Deleted the `POST /resources/get/{dataFolder?}` and `GET /resources/get-installer` endpoints as part of the architectural shift towards simplified resource management.
- Removed associated methods and configurations, including `ResourcesService.GetEncryptedResource`, `ResourcesService.GetInstaller`, and related properties in `ResourcesConfig`.
- Cleaned up environment variables and configuration files to reflect the removal of installer-related settings.
- Eliminated the `GetResourceRequest` DTO and its validator, along with the `WrongResourceName` error code.
- Updated documentation to clarify the changes in resource handling and the retirement of per-user file encryption.
Co-authored-by: Cursor <cursoragent@cursor.com>
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 /logoutendpoint: revokes the caller's current session (refresh + all access tokens minted from it). Idempotent.POST /logout/allendpoint: 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:revokedfor sub-second propagation. Pull is mandatory; push is best-effort acceleration.
- Pull mode (default):
Scope
Included
POST /logout,POST /logout/all,POST /sessions/{sid}/revokehandlers inAzaion.AdminApi/Program.cs.GET /sessions/revoked?since=<ts>endpoint authenticated via service-to-service JWT issued to each verifier identity (each verifier has a dedicatedRole=Serviceuser).- Update
sessionstable withrevoked_at,revoked_reason,revoked_by_user_id(these columns may already be present from AZ-531; if so, this ticket only addsrevoked_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=Serviceuser 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.