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 de credenciales y configuracion de MFA. Posee PasswordCredential y MfaEnrollment como entidades propias.
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 debe tener una PasswordCredential activa.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 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 |
| 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 |
| 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 |
| SetPasswordCommand | Crear o rotar credencial de contrasena activa |
| DeactivatePasswordCommand | Desactivar credencial (ej. en federacion de cuenta) |
| 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
│ ├── 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
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: SetPasswordCommand(userId, plainPassword, actorId)
H->>R: GetById(userId)
R-->>H: UserAccount
H->>P: Hash(plainPassword)
P-->>H: bcryptHash
H->>U: userAccount.SetPassword(credentialId, 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 UserId PK
uniqueidentifier TenantId FK
uniqueidentifier BranchId "Nullable FK"
nvarchar Email "Unico por TenantId"
nvarchar UserCategory "INTERNAL-EXTERNAL-B2B-PARTNER"
nvarchar Status "PENDING-ACTIVE-BLOCKED"
nvarchar IdentityReference "Nullable"
nvarchar IdentityReferenceType "Nullable - HR_ID-VENDOR_CODE-GOVERNMENT_ID-PARTNER_REF"
datetime2 CreatedAt
uniqueidentifier CreatedBy
datetime2 UpdatedAt
uniqueidentifier UpdatedBy
}
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 |
| SetPasswordCommand | userId, plainPassword, actorId | void |
| EnrollMfaCommand | userId, method, actorId | Guid enrollmentId, setupToken |
| VerifyMfaCommand | userId, enrollmentId, otp, actorId | void |
| Consulta | Retorna |
|—|—|
| GetUserMfaEnrollmentsQuery(userId) | List<MfaEnrollmentDto> |
| 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 |
| 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_RESTOREDPASSWORD_SET — registrado con actorId, userId, timestamp. Hash nunca registrado.MFA_ENROLLED, MFA_VERIFIED