---
title: SAIF.NetCore.NtlmApiAuth architecture
summary: Architectural responsibilities, boundaries, and interaction patterns.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.NtlmApiAuth
reference_type: package
---

# 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 `RoleRequirement`s → 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.

```mermaid
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.

```mermaid
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*.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Security and authorization](security-authorization.md)
- [Service registration](service-registration.md)
- [API reference](api/index.md)
- [SAIF.NetCore.AuthService overview](../saif-netcore-authservice/overview.md)
- [SAIF.NetCore.ServiceContracts overview](../saif-netcore-servicecontracts/overview.md)

> **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`
