Skip to content
bitzorcas
中EN

Concept

Identity Module Architecture: Multi-Tenant Identity Core

Deep dive into the BitzOrcas.Modern Identity Subsystem. Learn multi-tenant user aggregates, Argon2id password hashing, TOTP two-factor authentication, and enterprise SSO/OIDC.

Last updated

In modern enterprise multi-tenant B2B SaaS platforms, the Identity Subsystem is the central security backbone:

  • One User, Multiple Tenants: A single user (e.g. corporate consultant or auditor) may belong to multiple tenant organizations with different roles and permissions in each;
  • Enterprise Security Baseline: Defending against credential stuffing and weak passwords while providing out-of-the-box TOTP 2FA, operator impersonation, and enterprise SSO (SAML2 / OIDC);
  • Stateless High-Throughput: Sub-millisecond login verifications with distributed token revocation and refresh mechanisms.

BitzOrcas.Modern Identity Module adopts Vertical Slice Architecture and multi-tenant domain isolation, delivering an impenetrable security foundation.

Identity Subsystem Interaction Topology

1. Client (Web / Mobile / OpenAPI)

2. Identity API Endpoints (Login / Register / 2FA / Switch Tenant)

3. Vertical Slice Handlers (LoginCommandHandler / RegisterUserCommandHandler)

4. UserAggregate (Password Hashes / Lifecycle / Tenant Memberships)

5. Security Engines (IPasswordHasher: Argon2id, ITotpService)

6. ITokenIssuer (AccessToken & RefreshToken Issuance)

7. CAP Domain Events (user.registered / user.logged_in)


Step 1: Core Domain Entity UserAggregate

The user aggregate root adopts the Unified Aggregate pattern, mapping directly to SysUser:

UserAggregate.cs: Identity User Aggregate Root
using System;
using System.Collections.Generic;
using System.ComponentModel;
using BitzOrcas.Domain.Entities;
using BitzOrcas.Domain.Results;
using BitzOrcas.Persistence.Metadata;
namespace BitzOrcas.Identity.Domain;
public static class UserErrors
{
public static readonly Error InvalidEmail =
Error.Validation("User.InvalidEmail", "Invalid email format.");
}
[BitzTable("SysUser", IsTenant = false, IsSoftDelete = true, Description = "Global Platform Users")]
[BitzIndex("IX_SysUser_Email", nameof(Email), IsUnique = true)]
public sealed class UserAggregate : AggregateRoot<string>
{
[BitzColumn(Length = 120, IsRequired = true)]
public string Email { get; private set; } = string.Empty;
[BitzColumn(Length = 200, IsRequired = true)]
public string PasswordHash { get; private set; } = string.Empty;
[BitzColumn(Length = 50)]
public string PhoneNumber { get; private set; } = string.Empty;
// Global user state: Active, Suspended, PendingActivation
[BitzColumn(IsRequired = true)]
public UserStatus Status { get; private set; } = UserStatus.PendingActivation;
// Declares whether TOTP 2FA is active
[BitzColumn(IsRequired = true)]
public bool IsTwoFactorEnabled { get; private set; }
// Multi-tenant membership collection
private readonly List<UserTenantMembership> _memberships = [];
public IReadOnlyCollection<UserTenantMembership> Memberships => _memberships.AsReadOnly();
[Obsolete("For ORM materialization only. Use Create.", error: true)]
[EditorBrowsable(EditorBrowsableState.Never)]
public UserAggregate()
: base("0")
{
}
private UserAggregate(string id, string email, string passwordHash)
: base(id)
{
Email = email;
PasswordHash = passwordHash;
Status = UserStatus.Active;
}
// Domain factory method creating users
public static Result<UserAggregate> Create(string email, string passwordHash, string initialTenantId)
{
// 1. In-memory invariant verification
if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
{
return Result<UserAggregate>.Failure(UserErrors.InvalidEmail);
}
var user = new UserAggregate(Guid.NewGuid().ToString("N"), email.Trim().ToLowerInvariant(), passwordHash);
// 2. Bind initial tenant organization
user._memberships.Add(new UserTenantMembership(initialTenantId, isDefault: true));
// 3. Emit user registered domain event
user.AddDomainEvent(new UserRegisteredDomainEvent(user.Id, user.Email, initialTenantId));
return Result<UserAggregate>.Success(user);
}
}

Step 2: Vertical Slice Use Case (LoginCommandHandler)

The login command is completely self-contained within its vertical slice:

LoginCommandHandler.cs: Login Vertical Slice
using System.Threading;
using System.Threading.Tasks;
using BitzOrcas.Application.Abstractions.Security;
using BitzOrcas.Domain.Results;
using BitzOrcas.Identity.Contracts;
public static class AuthErrors
{
public static readonly Error InvalidCredentials =
Error.Unauthorized("Auth.InvalidCredentials", "Invalid email or password.");
public static readonly Error AccountSuspended =
Error.Forbidden("Auth.AccountSuspended", "Account is suspended or pending activation.");
}
public sealed class LoginCommandHandler(
IUserRepository userRepository,
IPasswordHasher passwordHasher,
ITokenService tokenService)
{
public async ValueTask<Result<LoginResponseDto>> Handle(LoginCommand command, CancellationToken ct)
{
// 1. Retrieve user by canonical email
var user = await userRepository.FindByEmailAsync(command.Email.ToLowerInvariant(), ct);
if (user is null)
{
// Constant-time execution prevents timing attacks
return Result<LoginResponseDto>.Failure(AuthErrors.InvalidCredentials);
}
// 2. Verify password hash using Argon2id
var isPasswordValid = passwordHasher.Verify(command.Password, user.PasswordHash);
if (!isPasswordValid)
{
return Result<LoginResponseDto>.Failure(AuthErrors.InvalidCredentials);
}
// 3. Check account lifecycle status
if (user.Status != UserStatus.Active)
{
return Result<LoginResponseDto>.Failure(AuthErrors.AccountSuspended);
}
// 4. Issue tenant-scoped JWT & Refresh Token pair
var tokenPair = await tokenService.GenerateTokensAsync(user, command.TenantId, ct);
return Result<LoginResponseDto>.Success(new LoginResponseDto(
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
ExpiresInSeconds: tokenPair.ExpiresIn));
}
}

Summary

The Identity Subsystem adheres to strict enterprise design standards:

  • Security-First: Default Argon2id hashing prevents rainbow table exploits;
  • Multi-Tenant Agility: Seamless switching between corporate tenants with a single identity;
  • Event-Driven Decoupling: Lifecycle changes emit transactional events asynchronously.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%