SAIF.NetCore.NtlmApiAuth architecture¶
SAIF.NetCore.NtlmApiAuth is an implementation-layer library in the Framework.NCore dependency graph that works around the NTLM/Kerberos "double hop" problem by letting a calling application forward an end user's name in an OnBehalfOfUser header, and letting the receiving API authorize both the calling (service-account) identity and the forwarded end-user identity against SecurityAPI AD roles; it is one of the three documented implementation-to-implementation exceptions in Architecture.md because it takes a direct NuGet package dependency on SAIF.NetCore.AuthService in addition to the foundation package SAIF.NetCore.ServiceContracts.
Responsibilities¶
- Convert an inbound
OnBehalfOfUserHTTP header into a claim on the authenticated identity (OnBehalfOfUserClaimMiddleware→OnBehalfOfUserClaimService). - Build named ASP.NET Core authorization policies out of one
RoleRequirementper role, and evaluate each requirement by delegating to a dual-identity authorization check (ConfigureRoleAuthorizationOptions,RoleRequirement,NtlmApiAuthorizationService). - Perform the dual-identity authorization decision itself: authorize the request user's role, and — if an on-behalf-of user is present — require that user's role too, both via
SAIF.NetCore.AuthService'sISecurityClient.IsAuthorized. - Provide client-side helpers so an outbound caller can resolve the current user's name (
ICurrentUserService), attach theOnBehalfOfUserheader to anIHttpJsonClient(ConfigureOnBehalfOfUserAuthorization), or forward an inbound header to a downstream API (ForwardOnBehalfOfUserHeader). - Optionally expose the
OnBehalfOfUserheader as a Swagger UI parameter when a debugger is attached (AddDebuggerOnBehalfOfHeader).
Component boundaries¶
- Server side:
OnBehalfOfUserClaimMiddleware/OnBehalfOfUserClaimService(header→claim),ConfigureRoleAuthorizationOptions(consumer-subclassed policy factory),RoleRequirement(self-handlingIAuthorizationRequirement/AuthorizationHandler<T>), andNtlmApiAuthorizationService(the only type that calls out of the library, viaISecurityClient.IsAuthorized). - Client side:
ICurrentUserService(MvcCurrentUserService,BlazorCurrentUserService),IHttpJsonClientExtensions.ConfigureOnBehalfOfUserAuthorization, andHttpContextAccessorExtensions.ForwardOnBehalfOfUserHeader— none of these call AuthService; they only read/write headers and options on a consumer-ownedIHttpJsonClient. - Boundary with
SAIF.NetCore.AuthService:NtlmApiAuthorizationServiceis the sole caller ofISecurityClient.IsAuthorized; all suffix-rewrite,IHttpJsonClient.GetAsync, and SecurityAPI wire-format logic is owned by AuthService'sSecurityClient, not by this library. - Boundary with
SAIF.NetCore.ServiceContracts: the library consumesIHttpJsonClient,HttpClientOptions, andIEnvironmentClientas contracts only;IEnvironmentClientis injected intoNtlmApiAuthorizationServicebut never called*. - Boundary with the consuming application: the application supplies Windows/NTLM authentication, a
ConfigureRoleAuthorizationOptionssubclass (SetPolicies), and registersISecurityClient/IEnvironmentClient/IHttpJsonClient/IHttpContextAccessor— none of these are registered by this library itself.
Interaction model¶
- Server-side request flow: host Windows authentication populates
HttpContext.User→OnBehalfOfUserClaimMiddlewareadds theOnBehalfOfUserclaim → ASP.NET Core authorization evaluates the named policy'sRoleRequirements → eachRoleRequirementcallsNtlmApiAuthorizationService.IsAuthorized→ that service calls AuthServiceISecurityClient.IsAuthorizedonce for the request user and, if present, once more for the on-behalf-of user. - Client-side flow: a consumer resolves
ICurrentUserService.GetCurrentUser()and callsIHttpJsonClient.ConfigureOnBehalfOfUserAuthorization(ICurrentUserService currentUserService)to attach the header to outbound requests, or callsIHttpContextAccessor.ForwardOnBehalfOfUserHeader(IHttpJsonClient httpJsonClient)to relay an inbound header to a downstream API call. - The only outbound network call reachable from this library is the transitive chain into AuthService's
SecurityClient.IsAuthorized, which issuesGET /v1/Users/IsAuthorized/{userId}/{suffixRole}against SecurityAPI viaIHttpJsonClient.GetAsync; this was confirmed by inspecting the repository's 10.xSecurityClientsource, while the package actually consumed by this library is the pinned8.2023.319.2release, so its applicability to that older version is inferred*. - No type in this library registers
ISecurityClient,IEnvironmentClient,IHttpJsonClient,IHttpContextAccessor, or authentication itself; the consuming application must register all of them alongside callingAddNtlmApiAuthorization<T>().
Architecture diagram¶
The following diagram shows NtlmApiAuth's position relative to its one implementation-layer exception dependency, the foundation layer, and the rest of the Framework.NCore dependency graph.
flowchart TB
SC["SAIF.NetCore.ServiceContracts<br>(IHttpJsonClient, IEnvironmentClient)"]
AUTH["SAIF.NetCore.AuthService<br>(ISecurityClient, SecurityClient)"]
NTLM["SAIF.NetCore.NtlmApiAuth<br>(NtlmApiAuthorizationService, RoleRequirement,<br>OnBehalfOfUserClaimMiddleware)"]
SECAPI["SecurityAPI<br>GET /v1/Users/IsAuthorized/{userId}/{suffixRole}"]
APP["Consuming application<br>(server and/or client role)"]
NTLM -.->|NuGet ref| SC
NTLM -.->|NuGet ref: documented exception| AUTH
AUTH -->|IHttpJsonClient.GetAsync| SECAPI
APP -.->|NuGet ref| NTLM
Sequence diagram¶
The following diagram shows the server-side dual-identity authorization sequence for one RoleRequirement evaluation.
sequenceDiagram
participant Client
participant Middleware as OnBehalfOfUserClaimMiddleware
participant Requirement as RoleRequirement
participant NtlmSvc as NtlmApiAuthorizationService
participant SecClient as AuthService: SecurityClient
participant SecurityAPI
Client->>Middleware: HTTP request (OnBehalfOfUser header)
Middleware->>Middleware: Add OnBehalfOfUser claim to identity
Middleware->>Requirement: next(context) — authorization evaluation
Requirement->>NtlmSvc: IsAuthorized(requestUser, onBehalfOfUser, role)
NtlmSvc->>SecClient: IsAuthorized(requestUser, role)
SecClient->>SecurityAPI: GET /v1/Users/IsAuthorized/{userId}/{suffixRole}
SecurityAPI-->>SecClient: bool
SecClient-->>NtlmSvc: bool
NtlmSvc->>SecClient: IsAuthorized(onBehalfOfUser, role) (if present)
SecClient->>SecurityAPI: GET /v1/Users/IsAuthorized/{userId}/{suffixRole}
SecurityAPI-->>SecClient: bool
SecClient-->>NtlmSvc: bool
NtlmSvc-->>Requirement: both authorized?
Requirement-->>Middleware: Succeed(requirement) or Fail()
Middleware-->>Client: HTTP response
Design constraints¶
- Multi-role policies evaluate as AND across all requirements in a policy, and
RoleRequirement.HandleRequirementAsyncfails the entire authorization on any single miss, even though the README describes OR semantics*. - Because
RoleRequirementis a self-handling requirement (AuthorizationHandler<RoleRequirement>), each instance's handler loops over every requirement in the authorization context and evaluates its own role against each — with N roles, SecurityAPI can be called up to 2·N² times per request*. - The header trust model has no allow-list of caller accounts: any authenticated caller can set
OnBehalfOfUser, and escalation is bounded only by both identities needing to pass every role check. NtlmApiAuthorizationServicecaptures its injectedISecurityClientfor the lifetime of the options-bound policy, which is safe only while AuthService registersISecurityClientas a singleton*.ConfigureOnBehalfOfUserAuthorizationreplaces the targetIHttpJsonClient's options wholesale (PersistHeadersAfterRequest = true), so if that client is registered as a singleton shared across users, one user's forwarded name can leak into later requests*.
Related documentation¶
- Index
- Overview
- Dependencies
- Operations and processes
- Business rules
- Security and authorization
- Service registration
- API reference
- SAIF.NetCore.AuthService overview
- SAIF.NetCore.ServiceContracts overview
Note: Statements marked with
*were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.
Sources¶
src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/SAIF.NetCore.NtlmApiAuth.csprojsrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Middleware/OnBehalfOfUserClaimMiddleware.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Authorization/RoleRequirement.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs,Extensions/HttpContextAccessorExtensions.cssrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.csArchitecture.md