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>
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) andLockoutUntil(active lockout deadline). Both reset on successful login.- AZ-534 (TOTP 2FA):
MfaEnabled,MfaSecret(encrypted viaIDataProtector),MfaRecoveryCodes(JSONB array of{ hash, used_at }),MfaEnrolledAt,MfaLastUsedWindow(RFC 6238 time-step counter — defends in-window replay).
MfaEnabled,MfaSecret,MfaRecoveryCodes, andMfaLastUsedWindoware[JsonIgnore]— they never leave the server in API responses.PasswordHashis also[JsonIgnore](this attribute was always there).The
PasswordHashcolumn 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 withUser AzaionDbexposesITable<User>AzaionDbSchemaHoldermapsUserto theusersPostgreSQL table;MfaRecoveryCodescarries an explicitDataType.BinaryJsonmapping so Npgsql sends the JSON oid (otherwise inserts fail with "column is of type jsonb but expression is of type text")SetUserQueueOffsetsRequestusesUserQueueOffsetsSessionrows referenceUserviaUserId(and viaAircraftIdfor mission sessions targetingRoleEnum.CompanionPCusers)
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
PasswordHashstores Argon2id PHC strings for new + rehashed users; legacy SHA-384 still accepted (lazy-migrated on next successful login).MfaSecretis encrypted at rest viaIDataProtector(purposeAzaion.Mfa.Secret.v1).MfaRecoveryCodesare SHA-256-hashed at rest; the plaintext list is shown only in the/users/me/mfa/enrollresponse.MfaLastUsedWindowdefends against in-window replay of the same TOTP code.FailedLoginCount+LockoutUntilenforce CMMC AC.L2-3.1.8 (lockout after 10 consecutive failed logins; 15-min default duration).Hardwareis 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.