TE-01: JWT / OIDC Authentication Flow
| Field |
Value |
| TE ID |
TE-01 |
| Status |
Approved |
| ADR Reference |
ADR-0020 (IdP Abstraction), ADR-0026 (MFA Adaptive) |
| Satisfies |
FS-01 (User Authentication), FS-08 (Hosted Login Redirection), FS-09 (MFA / Passwordless Adaptive Auth) |
| Owner |
Platform Team |
| Date |
2026-05-18 |
Problem
UMS must authenticate users from multiple identity providers (internal bcrypt, Zitadel, Azure AD, Okta, SAML2, OIDC) without coupling the domain or application layer to any specific SDK. Additionally, MFA enforcement must adapt per tenant policy without changing the core authentication flow.
Solution: Injectable IdP Port + Token Normalization
The IAuthenticationPort (Strategy Pattern) abstracts every IdP behind a single domain interface. The port contract returns a normalized AuthenticationResult carrying a standard JWT regardless of the upstream provider. The gateway handles protocol translation (SAML → OIDC, LDAP → internal token) before reaching application logic.
User Agent
│
▼
┌──────────────────────────────────────────────────────┐
│ Auth Gateway (Reverse Proxy / BFF) │
│ - Routes by tenant domain hint (IdP routing table) │
│ - Handles hosted login page (BC-C config) │
│ - Calls selected IdP adapter │
└──────────────┬───────────────────────────────────────┘
│ IAuthenticationPort.AuthenticateAsync()
▼
┌─────────────────────────────────────┐
│ IdP Adapter (selected at runtime) │
│ Internal │ Zitadel │ Azure │ SAML │
└──────────────────┬──────────────────┘
│ AuthenticationResult { UserId, TenantId, Claims }
▼
┌──────────────────────────────────────┐
│ Token Service (UMS Core API) │
│ - Validate result │
│ - Enforce MFA policy (FS-09) │
│ - Issue signed JWT (RS256) │
│ - Raise AuthenticationAttemptedEvent│
└──────────────────────────────────────┘
JWT Structure
| Claim |
Value |
Notes |
sub |
UserAccountId (Guid) |
UMS principal ID |
tid |
TenantId (Guid) |
Root tenant scope |
bid |
BranchId (Guid, nullable) |
Branch scope for BRANCH_SCOPED profiles |
cat |
UserCategory |
INTERNAL / EXTERNAL / SERVICE_ACCOUNT |
idp |
IdP strategy name |
e.g., ZITADEL, INTERNAL_BCRYPT |
exp |
Unix timestamp |
Configurable per tenant via BC-C |
jti |
UUID |
For replay prevention |
MFA Adaptive Enforcement (FS-09)
MFA is evaluated after primary authentication succeeds, before JWT issuance:
- Load tenant MFA policy from BC-C (
IConfigCachePort).
- Check
UserAccount.MfaEnrollments for an enrolled and verified method.
- If policy =
REQUIRED and no verified enrollment → return MFA_ENROLLMENT_REQUIRED.
- If policy =
CONDITIONAL and risk score ≥ threshold → challenge with enrolled method.
- On MFA success → raise
MfaVerifiedEvent.
Port Contract
public interface IAuthenticationPort
{
Task<AuthenticationResult> AuthenticateAsync(
AuthenticationRequest request,
CancellationToken cancellationToken = default);
}
public record AuthenticationRequest(
string IdpStrategy,
string Identifier,
string? Credential,
Guid TenantId);
public record AuthenticationResult(
bool Success,
Guid? UserId,
string? FailureReason,
IReadOnlyDictionary<string, string> Claims);
Implementation Sequence
- Infrastructure registers adapters via DI:
services.AddIdpAdapter<ZitadelAdapter>("ZITADEL").
IAuthenticationPort implementation resolves the correct adapter from the tenant’s active IdentityProvider strategy.
- Token service validates result, checks MFA, and returns a signed JWT.
- All outcomes (success and failure) are recorded via
AuthenticationAttemptedEvent → Audit context.
Coverage Gaps
| Gap |
Status |
SERVICE_ACCOUNT client_credentials flow (V6 in design-decisions.md) |
Pending design decision |
| Passwordless magic-link fallback |
Defined in BrandingSettings.MagicLinkFallback; adapter pending |