Files
admin/_docs/02_document/modules/common_entities_user.md
T
Oleksandr Bezdieniezhnykh a77b3f8a59 [AZ-529] [AZ-530] Cycle-2 documentation refresh
Refreshes _docs/02_document/ to reflect the cycle-2 auth-modernization
+ CMMC hardening landings (AZ-531..AZ-538). Authoritative source for
the ripple set is ripple_log_cycle2.md.

Covered:
- architecture.md (section 1 rewritten, ADRs 6-9 added)
- data_model.md (sessions, audit_events, user columns, migrations)
- system-flows.md (F1 rewritten; F11-F17 added; F2/F7/F9 minor)
- module-layout.md (cycle-2 sub-component table)
- diagrams/flows/flow_login.md (dual-token + MFA)
- components/{01_data_layer,03_auth_and_security,05_admin_api}
- modules/ (12 new, 8 modified — full Argon2id/ES256/MFA/refresh
  /mission/session/audit/jwks rollup)
- tests/{blackbox,security,traceability-matrix}

Step 13 (Update Docs) output for cycle 2.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 09:22:53 +03:00

5.6 KiB

Module: Azaion.Common.Entities.User

Purpose

Domain entity representing a system user, plus related value objects UserConfig and UserQueueOffsets.

Public Interface

Cycle 2 (2026-05-14) note — six new properties:

  • AZ-537 (CMMC AC.L2-3.1.8): FailedLoginCount (consecutive failed-login counter) and LockoutUntil (active lockout deadline). Both reset on successful login.
  • AZ-534 (TOTP 2FA): MfaEnabled, MfaSecret (encrypted via IDataProtector), MfaRecoveryCodes (JSONB array of { hash, used_at }), MfaEnrolledAt, MfaLastUsedWindow (RFC 6238 time-step counter — defends in-window replay).

MfaEnabled, MfaSecret, MfaRecoveryCodes, and MfaLastUsedWindow are [JsonIgnore] — they never leave the server in API responses. PasswordHash is also [JsonIgnore] (this attribute was always there).

The PasswordHash column now holds an Argon2id PHC string for new + rehashed users (AZ-536); legacy SHA-384 entries still validate and are transparently upgraded on next successful login.

User

Property Type Description
Id Guid Primary key
Email string Unique user email
PasswordHash string Argon2id PHC string ($argon2id$…) for new users; legacy 64-char Base64 SHA-384 still accepted by Security.VerifyPassword
Hardware string? TOMBSTONED — kept nullable, not read or written by any code path (AZ-197 removed the hardware-binding feature)
Role RoleEnum Authorization role
CreatedAt DateTime Account creation timestamp
LastLogin DateTime? Currently unused — left for forward compatibility
UserConfig UserConfig? JSON-serialized user configuration
IsEnabled bool Account active flag
FailedLoginCount int AZ-537 — consecutive failed-login counter; resets to 0 on success
LockoutUntil DateTime? AZ-537 — active lockout deadline (UTC). >= now() blocks login even with correct password
MfaEnabled bool AZ-534 — true after /users/me/mfa/confirm succeeds
MfaSecret string? AZ-534 — base32 TOTP secret encrypted at rest via IDataProtector (purpose Azaion.Mfa.Secret.v1)
MfaRecoveryCodes string? AZ-534 — JSONB array of { Hash, UsedAt }
MfaEnrolledAt DateTime? AZ-534 — set by Confirm
MfaLastUsedWindow long? AZ-534 — RFC 6238 time-step counter of the most recently accepted code; rejects in-window replay
Method Signature Description
GetCacheKey static string GetCacheKey(string email) Returns cache key "User.{email}"

UserConfig

Property Type Description
QueueOffsets UserQueueOffsets? Annotation queue offset tracking

UserQueueOffsets

Property Type Description
AnnotationsOffset ulong Offset for annotations queue
AnnotationsConfirmOffset ulong Offset for annotation confirmations
AnnotationsCommandsOffset ulong Offset for annotation commands

Internal Logic

GetCacheKey returns empty string for null/empty email to avoid cache key collisions.

Dependencies

  • RoleEnum

Consumers

  • All services (UserService, AuthService, ResourcesService, MfaService, MissionTokenService) work with User
  • AzaionDb exposes ITable<User>
  • AzaionDbSchemaHolder maps User to the users PostgreSQL table; MfaRecoveryCodes carries an explicit DataType.BinaryJson mapping so Npgsql sends the JSON oid (otherwise inserts fail with "column is of type jsonb but expression is of type text")
  • SetUserQueueOffsetsRequest uses UserQueueOffsets
  • Session rows reference User via UserId (and via AircraftId for mission sessions targeting RoleEnum.CompanionPC users)

Data Models

Maps to PostgreSQL table users with columns: id, email, password_hash, hardware, role, user_config (JSON text), created_at, last_login, is_enabled, failed_login_count (AZ-537), lockout_until (AZ-537), mfa_enabled (AZ-534), mfa_secret (AZ-534), mfa_recovery_codes (jsonb, AZ-534), mfa_enrolled_at (AZ-534), mfa_last_used_window (AZ-534).

Migration files: env/db/02_structure.sql (initial), 03_add_timestamp_columns.sql, 06_users_email_unique.sql (UNIQUE INDEX on email), 07_auth_lockout_and_audit.sql (AZ-537 lockout columns + audit_events table), 10_users_mfa.sql (AZ-534 MFA columns).

Configuration

None directly. MfaSecret encryption depends on the application-level DataProtection:KeysFolder setting (Production must point this at a persistent volume).

External Integrations

None directly — but MfaSecret depends on ASP.NET Core DataProtection for at-rest encryption.

Security

  • PasswordHash stores Argon2id PHC strings for new + rehashed users; legacy SHA-384 still accepted (lazy-migrated on next successful login).
  • MfaSecret is encrypted at rest via IDataProtector (purpose Azaion.Mfa.Secret.v1).
  • MfaRecoveryCodes are SHA-256-hashed at rest; the plaintext list is shown only in the /users/me/mfa/enroll response.
  • MfaLastUsedWindow defends against in-window replay of the same TOTP code.
  • FailedLoginCount + LockoutUntil enforce CMMC AC.L2-3.1.8 (lockout after 10 consecutive failed logins; 15-min default duration).
  • Hardware is a tombstone (no application code reads or writes it) per AZ-197.

Tests

Indirectly tested end-to-end via e2e/Azaion.E2E/Tests/LoginTests.cs, UserManagementTests.cs, DeviceTests.cs, RateLimitLockoutTests.cs, MfaEnrollmentTests.cs, MfaLoginTests.cs.