Idioma: English Español
Bounded Context: Identity
Aggregate Root: UserAccount
Modulo: Ums.Domain.Identity.UserAccount
Estado: Produccion
UserAccount representa la identidad digital de un usuario dentro de un Tenant. Es el punto central de autenticacion, gestion del ciclo de vida, borrado logico, credenciales y configuracion de MFA. Posee PasswordCredential y MfaEnrollment como entidades propias.
Pending.Active pero no tiene Profile activo.PasswordCredential) con historial de rotacion y asegurar rotacion segura.MfaEnrollment) enrollados por el usuario de forma independiente (TOTP, SMS, Email, WebAuthn).PasswordCredential: Almacena el hash BCrypt de la contrasena para autenticacion local. Soporta rotacion de credenciales con registros historicos (inactivos).
MfaEnrollment: Registra el enrolamiento de un usuario en un metodo MFA especifico. Se pueden enrolar multiples metodos por usuario, cada uno con su propio ciclo de vida (NotEnrolled, Enrolled, Verified).
UserAccount es su propio aggregate root. Todas las mutaciones de PasswordCredential y MfaEnrollment pasan por comandos de UserAccount.
Email debe ser unico dentro del mismo TenantId.Blocked no puede autenticarse.IdentityReference) no debe tener PasswordCredential activa.Pending no puede autenticarse; una contraseña local puede aprovisionarse antes de su activación.PasswordCredential con IsActive = true por usuario.PasswordHash debe ser un hash BCrypt valido. Las credenciales historicas se conservan para auditoria y no se eliminan.MfaMethod a lo sumo una vez — sin metodos duplicados.NotEnrolled, Enrolled, Verified. Un nuevo enrolamiento inicia en Enrolled y pasa a Verified al confirmar el desafio.UserAccount.Status no debe estar en Blocked ni Deleted para enrolar un nuevo metodo MFA.| Entidad / VO | Tipo | Notas |
|—|—|—|
| TenantId | Value Object | FK al Tenant propietario |
| BranchId | Value Object | FK opcional a Branch. Ya esta soportado en props, persistencia, contratos de aplicacion y flujo de creacion del agregado. |
| Email | Value Object | Unico por TenantId |
| UserCategory | Enum | INTERNAL · EXTERNAL · B2B · PARTNER |
| UserStatus | Enum | Pending · Active · Blocked · Deleted |
| IdentityReference | Value Object | Referencia maestra externa del sistema fuente autorizado (nullable) |
| IdentityReferenceType | Enum | HR_ID · VENDOR_CODE · GOVERNMENT_ID · PARTNER_REF (nullable) |
| AuditValueObject | Value Object | CreatedAt/By, UpdatedAt/By |
| Evento | Disparador |
|—|—|
| UserRegisteredEvent | Usuario registrado en el sistema |
| UserActivatedEvent | Usuario activado (Pending o Blocked -> Active) |
| UserBlockedEvent | Usuario bloqueado |
| UserRestoredEvent | Usuario restaurado desde bloqueado |
| UserDeletedEvent | Usuario borrado logicamente y desactivado |
| MfaEnrolledEvent | Nuevo metodo MFA enrollado |
| MfaVerifiedEvent | Desafio MFA completado exitosamente |
| AuthenticationAttemptedEvent | Intento de autenticacion registrado |
(Nota: Las operaciones de contrasena alimentan la auditoria y no tienen un evento dedicado propio)
| Comando | Descripcion |
|—|—|
| RegisterUserCommand | Registrar nuevo usuario |
| ActivateUserCommand | Activar usuario pendiente o bloqueado |
| BlockUserCommand | Bloquear usuario activo |
| RestoreUserCommand | Restaurar usuario bloqueado |
| DeleteUserCommand | Borrar logicamente la cuenta y anonimizar PII mediante persistencia del repositorio |
| AddUserAccountPasswordCommand | Crear o rotar la credencial local activa; el hash se genera en la API |
| EnrollMfaCommand | Enrolar nuevo metodo MFA |
| VerifyMfaCommand | Confirmar desafio MFA (Enrolled -> Verified) |
| LinkExternalIdentityCommand | Vincular identidad federada |
UserAccount (Aggregate Root)
├── Props: UserAccountProps
│ ├── Id: IdValueObject
│ ├── TenantId: TenantId
│ ├── BranchId?: BranchId
│ ├── Email: Email
│ ├── DisplayName?: Name
│ ├── Category: UserCategory
│ ├── Status: UserStatus
│ ├── IdentityReference?: IdentityReference
│ ├── IdentityReferenceType?: IdentityReferenceType
│ └── Audit: AuditValueObject
├── PasswordCredential (Entidad Propia, 0..N almacenadas, 0..1 activa)
│ └── Props: PasswordCredentialProps
│ ├── Id: IdValueObject
│ ├── UserAccountId: UserAccountId
│ ├── PasswordHash: PasswordHash
│ ├── IsActive: bool
│ └── Audit: AuditValueObject
└── MfaEnrollment (Entidad Propia, 0..N)
└── Props: MfaEnrollmentProps
├── Id: IdValueObject
├── UserAccountId: UserAccountId
├── Method: MfaMethod
├── Status: MfaEnrollmentStatus
└── Audit: AuditValueObject
UserAccount:
Pending -> Active -> Blocked -> Active
Active -> Deleted
PasswordCredential:
Nueva Credencial (IsActive = true)
↓ (en SetPassword)
Credencial Anterior (IsActive = false) — retenida para historial
MfaEnrollment:
NotEnrolled ──► Enrolled ──► Verified
sequenceDiagram
participant C as Cliente
participant H as RegisterUserHandler
participant R as IUserAccountRepository
C->>H: RegisterUserCommand(tenantId, email, category, createdBy)
H->>R: ExistsByEmail(tenantId, email)
R-->>H: false
H->>H: Crear UserAccount (Status = Pending)
H->>H: Emitir UserRegisteredEvent
H->>R: Add(userAccount)
H-->>C: userId
sequenceDiagram
participant C as Cliente
participant H as BlockUserHandler
participant R as IUserAccountRepository
participant U as UserAccount (AR)
C->>H: BlockUserCommand(userId, reason, actorId)
H->>R: GetById(userId)
R-->>H: UserAccount
H->>U: userAccount.Block(reason, actorId)
U->>U: Guardia: Status debe ser Active
U->>U: Status = Blocked
U->>U: Emitir UserBlockedEvent
H->>R: Update(userAccount)
H-->>C: void
sequenceDiagram
participant C as Cliente
participant H as SetPasswordHandler
participant R as IUserAccountRepository
participant U as UserAccount (AR)
participant P as IPasswordHashingService
C->>H: AddUserAccountPasswordCommand(userId, password)
H->>R: GetById(userId)
R-->>H: UserAccount
H->>P: Hash(plainPassword)
P-->>H: bcryptHash
H->>U: userAccount.AddPassword(bcryptHash, actorId)
U->>U: Buscar PasswordCredential activa
U->>U: Establecer IsActive = false en existente
U->>U: Crear nueva PasswordCredential (IsActive = true)
H->>R: Update(userAccount)
H-->>C: void
sequenceDiagram
participant H as LinkExternalIdentityHandler
participant R as IUserAccountRepository
participant U as UserAccount (AR)
H->>R: GetById(userId)
R-->>H: UserAccount
H->>U: userAccount.LinkExternalIdentity(ref, refType, actorId)
U->>U: Establecer IdentityReference + IdentityReferenceType
U->>U: Buscar PasswordCredential activa
U->>U: Establecer PasswordCredential.IsActive = false
H->>R: Update(userAccount)
sequenceDiagram
participant C as Cliente
participant H as EnrollMfaHandler
participant R as IUserAccountRepository
participant U as UserAccount (AR)
participant MFA as IMfaChallengeService
C->>H: EnrollMfaCommand(userId, method, actorId)
H->>R: GetById(userId)
R-->>H: UserAccount
H->>U: userAccount.EnrollMfa(method, actorId)
U->>U: Guardia: metodo no ya enrollado
U->>U: Guardia: usuario no bloqueado
U->>U: Crear MfaEnrollment (Status = Enrolled)
U->>U: Emitir MfaEnrolledEvent
H->>R: Update(userAccount)
H->>MFA: InitiateSetup(userId, method)
MFA-->>H: setupToken
H-->>C: enrollmentId, setupToken
sequenceDiagram
participant C as Cliente
participant H as VerifyMfaHandler
participant R as IUserAccountRepository
participant U as UserAccount (AR)
participant MFA as IMfaChallengeService
C->>H: VerifyMfaCommand(userId, enrollmentId, otp, actorId)
H->>MFA: Validate(userId, method, otp)
MFA-->>H: valido
H->>R: GetById(userId)
R-->>H: UserAccount
H->>U: userAccount.VerifyMfa(enrollmentId, actorId)
U->>U: Enrollment.Status = Verified
U->>U: Emitir MfaVerifiedEvent
H->>R: Update(userAccount)
H-->>C: void
erDiagram
TENANT ||--o{ USER_ACCOUNT : "tiene"
USER_ACCOUNT ||--o{ PASSWORD_CREDENTIAL : "autenticado_con"
USER_ACCOUNT ||--o{ MFA_ENROLLMENT : "enrolla_mfa"
USER_ACCOUNT {
uniqueidentifier Id PK
uniqueidentifier TenantId FK
uniqueidentifier BranchId "Nullable FK"
nvarchar Email "Unico por TenantId"
nvarchar DisplayName "Nullable"
int CategoryId "UserCategory"
int StatusId "1=Pending, 2=Active, 3=Blocked, 4=Deleted"
nvarchar IdentityReference "Nullable"
int IdentityReferenceTypeId "Nullable"
nvarchar CreatedBy
datetime2 CreatedAtUtc
nvarchar UpdatedBy "Nullable"
datetime2 UpdatedAtUtc "Nullable"
nvarchar AuditTimeSpan
rowversion RowVersion
bit IsDeleted
datetime2 DeletedAtUtc "Nullable"
nvarchar DeletedBy "Nullable"
datetime2 AnonymizedAtUtc "Nullable"
}
PASSWORD_CREDENTIAL {
uniqueidentifier CredentialId PK
uniqueidentifier UserAccountId FK
nvarchar PasswordHash "Hash BCrypt - solo escritura"
bit IsActive "Solo uno true por UserAccount"
datetime2 CreatedAt
uniqueidentifier CreatedBy
datetime2 UpdatedAt
uniqueidentifier UpdatedBy
}
MFA_ENROLLMENT {
uniqueidentifier MfaEnrollmentId PK
uniqueidentifier UserAccountId FK
nvarchar Method "TOTP-SMS-EMAIL-WEBAUTHN"
nvarchar Status "NOT_ENROLLED-ENROLLED-VERIFIED"
datetime2 CreatedAt
uniqueidentifier CreatedBy
datetime2 UpdatedAt
uniqueidentifier UpdatedBy
}
flowchart TD
subgraph Identity["Identity BC"]
T[Tenant AR]
UA[UserAccount AR]
PC[PasswordCredential Entity]
MFA[MfaEnrollment Entity]
UA -->|TenantId| T
UA --> PC
UA --> MFA
end
subgraph Authorization["Authorization BC"]
PROF[Profile AR]
end
subgraph Infra["Infrastructure"]
AUTH[Authentication Service]
HASH[Password Hashing Service]
TOTP[TOTP Service]
SMS[SMS Gateway]
WA[WebAuthn Authenticator]
end
subgraph Audit["Audit BC"]
AUD[AuditRecord]
end
PROF -->|UserId| UA
UA -->|eventos de dominio| AUD
HASH -->|Hash BCrypt| PC
AUTH -->|Verificar contrasena| PC
PC -->|Evento PASSWORD_SET| AUD
TOTP -->|Validacion OTP| MFA
SMS -->|Entrega OTP| MFA
WA -->|Verificacion de Asercion| MFA
MFA -->|MFA_ENROLLED| AUD
MFA -->|MFA_VERIFIED| AUD
| Comando | Entrada | Salida |
|—|—|—|
| RegisterUserCommand | tenantId, email, category, createdBy | Guid userId |
| ActivateUserCommand | userId, actorId | void |
| BlockUserCommand | userId, reason, actorId | void |
| AddUserAccountPasswordCommand | userId, password | Guid credentialId; la API genera el hash, el cliente nunca lo suministra |
| EnrollMfaCommand | userId, method, actorId | Guid enrollmentId, setupToken |
| VerifyMfaCommand | userId, enrollmentId, otp, actorId | void |
| Consulta | Retorna |
|—|—|
| GetUserMfaEnrollmentsQuery(userId) | List<MfaEnrollmentDto> |
| GetUserAccountByIdQuery(userId) | Estado hasActivePassword y passwordUpdatedAtUtc, nunca PasswordHash |
| Codigo | Condicion |
|—|—|
| USER_EMAIL_DUPLICATE | Email ya existe en el tenant |
| USER_NOT_FOUND | userId desconocido |
| USER_NOT_ACTIVE | Operacion requiere usuario activo |
| USER_IS_FEDERATED | No se puede establecer contrasena en usuario federado |
| PASSWORD_HASH_INVALID | Fallo en validacion del hash |
| MFA_METHOD_ALREADY_ENROLLED | Metodo MFA ya enrollado |
| MFA_ENROLLMENT_NOT_FOUND | enrollmentId desconocido |
| MFA_VERIFICATION_FAILED | OTP invalido |
| Concepto de Negocio | Estado Persistido | Notas |
|—|—|—|
| Solicitud de alta de usuario pendiente | UserStatus.Pending | El flujo publico de Phase 3 crea un UserAccount pendiente. |
| Alta de usuario aprobada | UserStatus.Active | La aprobacion se implementa mediante activacion. |
| Usuario activo sin perfil | UserStatus.Active mas ausencia de Profile activo | Es un estado derivado de lobby, no un UserStatus persistido. |
| Alta de usuario denegada | Resultado requerido por EP-09 | Aun se requiere comando dedicado de denegacion y motivo de ciclo de vida; Blocked existe pero no es semanticamente identico a denegacion de signup. |
| Indice | Columnas | Tipo |
|—|—|—|
| IX_UserAccount_TenantId_Email | TenantId, Email | Unico |
| IX_UserAccount_TenantId | TenantId | No unico |
| IX_PasswordCredential_UserAccountId_IsActive | UserAccountId, IsActive | No unico |
| IX_MfaEnrollment_UserAccountId_Method | UserAccountId, Method | Unico (solo activos) |
(UserAccountId, Method) — solo un enrolamiento por metodo por usuario activo.PasswordHash nunca debe aparecer en proyecciones de consultas retornadas a clientes.PasswordHash nunca debe aparecer en payloads AuditRecord.WhatChanged.| Operacion | Rol Requerido | |—|—| | Registrar Usuario | Tenant:Admin · Tenant:UserManager | | Bloquear / Restaurar | Tenant:Admin | | Establecer Contrasena | Usuario mismo o Tenant:Admin | | Leer Credencial (solo IsActive) | Tenant:Admin | | Leer Hash | Nadie — solo escritura | | Enrolar MFA | Usuario mismo | | Verificar MFA | Usuario mismo |
PasswordHash es el campo mas sensible del sistema. El acceso de lectura debe ser bloqueado a nivel de repositorio.Email es PII — enmascarado en logs.USER_REGISTERED, USER_ACTIVATED, USER_BLOCKED, USER_RESTORED, USER_DELETEDPASSWORD_SET — registrado con actorId, userId, timestamp. Hash nunca registrado.MFA_ENROLLED, MFA_VERIFIED