UMS-specific: No Evolith upstream. The Authorization Graph Engine is the core differentiator of UMS as a centralized auth platform. This decision is product-domain-exclusive and not intended for framework promotion.
Status: Accepted Date: 2026-05-31 Decision Owner: Architecture Related:
UMS’s primary objective is to serve as the central authentication and authorization engine for multi-tenant client systems. Prior to this decision, the login endpoint (POST /api/v1/auth/login) returned an empty Permissions: [] array and performed only BCrypt validation. Client systems received a JWT with no actionable authorization data, forcing them to implement their own permission logic or re-query UMS on every request.
The system needs to:
After authentication, UMS builds an AuthorizationGraph record containing:
AuthorizationGraph
├── context — user, tenant, systemSuite, role, profile, branch
├── authentication — method (Local|IDP), provider, mfaRequired, expiry
├── actions[] — all registered actions in the SystemSuite
├── menuAccess[] — Module→Menu→SubMenu→Option tree with AccessEffect per option
├── domainPermissions[] — domain resources with effect per action (Aggregate/Entity)
├── featureFlags[] — flags evaluated against user context at auth-time
├── effectiveConfig — tenant-resolved parameters (session timeout, MFA, etc.)
├── scopes[] — OAuth2-style "resourceCode.actionCode" from Allow permissions
├── generatedAt — UTC timestamp
└── validUntil — generatedAt + SESSION_TIMEOUT_MINUTES
The graph is immutable after construction. Client systems cache it for the session duration and make authorization decisions locally.
For each (TargetType, TargetId, ActionId) pair:
ProfilePermission where IsActive = trueIsOverride = true → use PP values (Source = Override)IsOverride = false → use original TemplateItem values (Source = Template)IsDenied = true → AccessEffect.Deny (always wins over Allow)IsAllowed = true → AccessEffect.AllowAccessEffect.NotGranted (implicit deny)POST /api/v1/auth/login — existing endpoint, now returns the full graph. Sets session cookie for the web frontend (backward compatible). Returns LoginSuccessResponse enriched with AuthorizationGraph.POST /api/v1/client/authenticate — new, stateless endpoint for external client systems. No cookie. Returns ClientAuthResponse with JWT + serialized graph.The graph is serialized in the format configured by the tenant (AUTH_GRAPH_DEFAULT_FORMAT parameter, default: JSON). Supported formats: JSON, XML, YAML, CSV.
Clients can override via ?format=xml query param or Accept header. New formats are added by registering an IAuthorizationGraphSerializer implementation in AuthorizationGraphSerializerFactorySetup — no application code changes needed.
Client systems need to check permissions multiple times per request (menu rendering, button visibility, API access guards). A self-contained graph eliminates N round-trips to UMS per session. The graph is valid for SESSION_TIMEOUT_MINUTES — the same duration as the session token.
JWT claims have a size limit and are static after issuance. The graph is rich (hundreds of permissions), needs to include dynamic data (feature flags), and must reflect the state at auth-time. The graph is serialized separately and embedded in the response body, not in the JWT (the JWT only carries compact summary claims).
Ums.Domain.Enums.PermissionEffect already exists as a DomainEnumeration with Allow and Deny. The graph needs a third value NotGranted (implicit deny — no permission entry exists). A new AccessEffect enum in the Graph namespace avoids extending the existing enumeration.
| Component | Location |
|---|---|
AuthorizationGraph record |
Ums.Domain/Authorization/Graph/ |
IAuthorizationGraphBuilder port |
Ums.Domain/Authorization/Graph/IAuthorizationGraphBuilder.cs |
AuthorizationGraphBuilderService |
Ums.Application/Authorization/Graph/AuthorizationGraphBuilderService.cs |
IAuthorizationGraphSerializer |
Ums.Application/Authorization/Graph/Serializers/ |
| JSON/XML/YAML/CSV serializers | Ums.Infrastructure/Authorization/Graph/ |
AuthorizationGraphSerializerFactorySetup |
Ums.Infrastructure/Authorization/Graph/ |
AuthGraphFormatProvider |
Ums.Application/Authorization/Graph/AuthGraphFormatProvider.cs |
POST /api/v1/auth/login |
Ums.Presentation/Endpoints/Identity/Auth/AuthEndpoints.cs |
POST /api/v1/client/authenticate |
Ums.Presentation/Endpoints/Identity/Auth/ClientAuthEndpoints.cs |
GET /api/v1/profiles/{id}/auth-graph/preview |
Ums.Presentation/Endpoints/Authorization/Profile/ProfileEndpoints.cs |
The endpoint GET /api/v1/profiles/{profileId}/auth-graph/preview uses the same IAuthorizationGraphBuilder pipeline as the external POST /client/authenticate flow. Credential validation is the only step omitted — the profile is resolved directly by ID.
This design guarantees that administrators see exactly the graph that would be produced for a real authentication request. See ADR-0080: Auth Graph Preview — Internal vs External Pipeline for full rationale.
Key behavioural differences from the external flow:
IUserContext.IsAuthenticated).Graph.Preview.Internal instead of Auth.Success.X-Preview-Mode: internal-preview and X-Request-Id response headers.?format= override query parameter.