Skip to content

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 OnBehalfOfUser HTTP header into a claim on the authenticated identity (OnBehalfOfUserClaimMiddleware → OnBehalfOfUserClaimService).
  • Build named ASP.NET Core authorization policies out of one RoleRequirement per 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's ISecurityClient.IsAuthorized.
  • Provide client-side helpers so an outbound caller can resolve the current user's name (ICurrentUserService), attach the OnBehalfOfUser header to an IHttpJsonClient (ConfigureOnBehalfOfUserAuthorization), or forward an inbound header to a downstream API (ForwardOnBehalfOfUserHeader).
  • Optionally expose the OnBehalfOfUser header 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-handling IAuthorizationRequirement/AuthorizationHandler<T>), and NtlmApiAuthorizationService (the only type that calls out of the library, via ISecurityClient.IsAuthorized).
  • Client side: ICurrentUserService (MvcCurrentUserService, BlazorCurrentUserService), IHttpJsonClientExtensions.ConfigureOnBehalfOfUserAuthorization, and HttpContextAccessorExtensions.ForwardOnBehalfOfUserHeader — none of these call AuthService; they only read/write headers and options on a consumer-owned IHttpJsonClient.
  • Boundary with SAIF.NetCore.AuthService: NtlmApiAuthorizationService is the sole caller of ISecurityClient.IsAuthorized; all suffix-rewrite, IHttpJsonClient.GetAsync, and SecurityAPI wire-format logic is owned by AuthService's SecurityClient, not by this library.
  • Boundary with SAIF.NetCore.ServiceContracts: the library consumes IHttpJsonClient, HttpClientOptions, and IEnvironmentClient as contracts only; IEnvironmentClient is injected into NtlmApiAuthorizationService but never called*.
  • Boundary with the consuming application: the application supplies Windows/NTLM authentication, a ConfigureRoleAuthorizationOptions subclass (SetPolicies), and registers ISecurityClient/IEnvironmentClient/IHttpJsonClient/IHttpContextAccessor — none of these are registered by this library itself.

Interaction model

  • Server-side request flow: host Windows authentication populates HttpContext.User → OnBehalfOfUserClaimMiddleware adds the OnBehalfOfUser claim → ASP.NET Core authorization evaluates the named policy's RoleRequirements → each RoleRequirement calls NtlmApiAuthorizationService.IsAuthorized → that service calls AuthService ISecurityClient.IsAuthorized once for the request user and, if present, once more for the on-behalf-of user.
  • Client-side flow: a consumer resolves ICurrentUserService.GetCurrentUser() and calls IHttpJsonClient.ConfigureOnBehalfOfUserAuthorization(ICurrentUserService currentUserService) to attach the header to outbound requests, or calls IHttpContextAccessor.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 issues GET /v1/Users/IsAuthorized/{userId}/{suffixRole} against SecurityAPI via IHttpJsonClient.GetAsync; this was confirmed by inspecting the repository's 10.x SecurityClient source, while the package actually consumed by this library is the pinned 8.2023.319.2 release, 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 calling AddNtlmApiAuthorization<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.HandleRequirementAsync fails the entire authorization on any single miss, even though the README describes OR semantics*.
  • Because RoleRequirement is 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.
  • NtlmApiAuthorizationService captures its injected ISecurityClient for the lifetime of the options-bound policy, which is safe only while AuthService registers ISecurityClient as a singleton*.
  • ConfigureOnBehalfOfUserAuthorization replaces the target IHttpJsonClient'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*.

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.csproj
  • src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Middleware/OnBehalfOfUserClaimMiddleware.cs
  • src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Authorization/RoleRequirement.cs
  • src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs
  • src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs
  • src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs, Extensions/HttpContextAccessorExtensions.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs
  • Architecture.md