ums

UserAccount — Arquitectura del Agregado

Idioma: English Español

Bounded Context: Identity Aggregate Root: UserAccount Modulo: Ums.Domain.Identity.UserAccount Estado: Produccion


1. Descripcion del Agregado

Proposito

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.

Responsabilidad de Negocio

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).

Aggregate Root

UserAccount es su propio aggregate root. Todas las mutaciones de PasswordCredential y MfaEnrollment pasan por comandos de UserAccount.

Invariantes y Reglas de Consistencia

  1. UserAccount: Email debe ser unico dentro del mismo TenantId.
  2. UserAccount: Un usuario en estado Blocked no puede autenticarse.
  3. UserAccount/PasswordCredential: Un usuario federado (con IdentityReference) no debe tener PasswordCredential activa.
  4. UserAccount/PasswordCredential: Un usuario en estado Pending no debe tener una PasswordCredential activa.
  5. PasswordCredential: A lo sumo una PasswordCredential con IsActive = true por usuario.
  6. PasswordCredential: Establecer una nueva contrasena desactiva automaticamente la credencial activa anterior.
  7. PasswordCredential: PasswordHash debe ser un hash BCrypt valido. Las credenciales historicas se conservan para auditoria y no se eliminan.
  8. MfaEnrollment: Un usuario puede enrolar cada MfaMethod a lo sumo una vez — sin metodos duplicados.
  9. MfaEnrollment: Los estados siguen el modelo implementado NotEnrolled, Enrolled, Verified. Un nuevo enrolamiento inicia en Enrolled y pasa a Verified al confirmar el desafio.
  10. MfaEnrollment: UserAccount.Status no debe estar en Blocked para enrolar un nuevo metodo MFA.
  11. MfaEnrollment: Al menos un metodo enrolado debe permanecer si el tenant requiere MFA.

Entidades Relacionadas / Value Objects

| 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 |

Eventos de Dominio

| 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)

Comandos / Casos de Uso

| 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 |


2. Modelo de Objetos

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

Ciclo de Vida

UserAccount:

Pending ──► Active ──► Blocked ──► Active

PasswordCredential:

Nueva Credencial (IsActive = true)
    ↓ (en SetPassword)
Credencial Anterior (IsActive = false) — retenida para historial

MfaEnrollment:

NotEnrolled ──► Enrolled ──► Verified

3. Diagramas de Secuencia

Flujo: Registrar Usuario

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

Flujo: Bloquear Usuario

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

Flujo: Establecer Contrasena

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

Flujo: Desactivar Credencial (en federacion)

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)

Flujo: Enrolar MFA

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

Flujo: Verificar MFA

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

4. Modelo Entidad-Relacion

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
    }

5. Modelo de Bounded Context

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

6. Contrato de Capa de Aplicacion

Comandos

| 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 |

Consultas

| Consulta | Retorna | |—|—| | GetUserMfaEnrollmentsQuery(userId) | List<MfaEnrollmentDto> |

Casos de Error

| 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 |


7. Notas de Persistencia

Indices

| 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) |

Seguridad y Restricciones Unicas


8. Seguridad y Auditoria

Reglas de Autorizacion

| 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 |

Datos Sensibles

Eventos de Auditoria

Cumplimiento