# Module: Azaion.Common.Entities.Session ## Purpose Domain entity representing one issued refresh token (interactive sessions) or one mission token (long-lived UAV sessions). One row per issued token; rotated rows chain via `ParentSessionId` and share a `FamilyId` so reuse-detection and family-wide revocation can key off it. > Added in cycle 2 (2026-05-14). Initial shape from AZ-531 (interactive refresh-token sessions); extended in the same cycle by AZ-535 (`RevokedByUserId`), AZ-533 (`Class`, `AircraftId`), and AZ-534 (`MfaAuthenticated`). ## Public Interface ### Session | Property | Type | Description | |----------|------|-------------| | `Id` | `Guid` | Primary key. | | `UserId` | `Guid` | FK to `users.id`. | | `RefreshHash` | `string?` | SHA-256 hex of the opaque refresh token. NULL for mission sessions (they have no refresh value). Unique-indexed. | | `FamilyId` | `Guid` | All rotations of the same login share this id. For interactive root rows and for mission rows, `FamilyId == Id`. | | `IssuedAt` | `DateTime` | Row creation time. | | `LastUsedAt` | `DateTime` | Updated on rotation; informational. | | `ExpiresAt` | `DateTime` | Sliding (interactive) or absolute (mission) expiry. | | `RevokedAt` | `DateTime?` | Set on rotation, reuse-detection, logout, admin revoke, post-flight reconnect. | | `RevokedReason` | `string?` | One of `SessionRevokedReasons`. | | `ParentSessionId` | `Guid?` | The previous row in the family (set on rotation). | | `FamilyStartedAt` | `DateTime` | First-issue time of the family — used for the absolute expiry check. | | `RevokedByUserId` | `Guid?` | AZ-535 — audit trail of who revoked the session. NULL for system revocations (rotation, reuse, post-flight). | | `Class` | `string` | AZ-533 — `"interactive"` (default) or `"mission"`. | | `AircraftId` | `Guid?` | AZ-533 — for mission sessions, the `CompanionPC` user the mission token belongs to. Used by `RevokeMissionsForAircraft`. | | `MfaAuthenticated` | `bool` | AZ-534 — pinned at issue; refresh rotation inherits the original AMR strength even if MFA is enabled/disabled mid-session. | ### SessionRevokedReasons (constants) | Value | When | |-------|------| | `rotated` | Old row marked as superseded by a successful refresh rotation. | | `reuse_detected` | OAuth 2.1 §6.1 — already-rotated refresh re-presented; whole family killed. | | `logged_out` | User called `POST /logout`. | | `logged_out_all` | User called `POST /logout/all`. | | `admin_revoked` | Admin called `POST /sessions/{sid}/revoke`. | | `post_flight_reconnect` | Aircraft reconnected; mission auto-revoked. | | `family_revoked` | Reserved (manual family-wide revocation; not currently emitted). | ### SessionClasses (constants) | Value | Meaning | |-------|---------| | `interactive` | Refresh-backed user session (AZ-531 default). | | `mission` | Long-lived no-refresh UAV mission token (AZ-533). | ## Internal Logic None — pure data class. All session lifecycle logic lives in `RefreshTokenService`, `SessionService`, `MissionTokenService`. ## Dependencies None. ## Consumers - `RefreshTokenService` — inserts root/family rows, updates on rotation/reuse-detection - `SessionService` — revocation paths and the verifier-poll snapshot - `MissionTokenService` — inserts mission-class rows - `AzaionDb.Sessions` — `ITable` access - `AzaionDbSchemaHolder` — maps `Session` to the `sessions` table ## Data Models Maps to PostgreSQL table `sessions` (defined in `env/db/08_sessions.sql`, extended by `09_sessions_logout_and_mission.sql` and `10_users_mfa.sql`). ## Configuration None. ## External Integrations None. ## Security - `refresh_hash` stores SHA-256 of the opaque token; the plaintext is never persisted. - The `family_id` partial index `sessions_family_active_idx WHERE revoked_at IS NULL` keeps reuse-detection and `RevokeAllForUser` cheap even as the revoked tail grows. - Auto-revoke-on-reconnect (`RevokeMissionsForAircraft`) closes the mission-token "lost UAV" risk when the aircraft phones home again; the partial index `sessions_aircraft_active_idx (aircraft_id, class) WHERE revoked_at IS NULL AND aircraft_id IS NOT NULL` keeps that check O(active mission rows). ## Tests Indirectly tested via `RefreshTokenTests`, `LogoutTests`, `MissionTokenTests`, and `MfaLoginTests` (which all exercise the entity through the service layer).