---
title: SAIF.NetCore.NtlmApiAuth security and authorization
summary: Authorization requirements, AD resource groups, security enforcement points, and data-level access control.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.NtlmApiAuth
reference_type: package
---

# SAIF.NetCore.NtlmApiAuth security and authorization

This library implements no authentication; it relies entirely on host Windows/NTLM (IIS) authentication for the caller identity. It is a dual-identity **authorization** add-on: every named policy role is checked against both the authenticated caller and an optional header-named "on behalf of" end user, with the actual AD-role decision delegated to `SAIF.NetCore.AuthService`'s remote SecurityAPI.

## Authorization architecture

Authorization is centralized and remote: this library builds ASP.NET Core policies locally but never decides a role check itself — every decision is a `GET` call to SecurityAPI made through AuthService's `ISecurityClient.IsAuthorized`, reached up to twice per requirement evaluation (once for the caller, once for the on-behalf-of user).

```mermaid
flowchart TD
    Header[OnBehalfOfUser header] --> Claim[OnBehalfOfUserClaimMiddleware adds claim]
    Claim --> Policy["Authorize Policy = name"]
    Policy --> Req[RoleRequirement.HandleRequirementAsync]
    Req --> Svc[NtlmApiAuthorizationService.IsAuthorized]
    Svc --> Sec["AuthService: SecurityClient.IsAuthorized"]
    Sec --> API["SecurityAPI GET /v1/Users/IsAuthorized/user/role"]
    API --> AD["(Active Directory group membership)"]
```

## AD resource catalog

| Resource name | Purpose | Enforcement point | Source |
| --- | --- | --- | --- |
| *(none hard-coded)* | Roles are consumer-supplied per named policy via `ConfigureRoleAuthorizationOptions.AddPolicies`; each role is checked for the caller and, if present, the on-behalf-of user. | `ConfigureRoleAuthorizationOptions.AddPolicies` → `RoleRequirement.HandleRequirementAsync` → `NtlmApiAuthorizationService.IsAuthorized` → AuthService `ISecurityClient.IsAuthorized` | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs`, `Authorization/RoleRequirement.cs`, `Services/NtmlApiAuthorizationService.cs` |

No AD group or resource-name literal exists in library code. Every environment-suffix rewrite (e.g. `XResource` → `XTestResource`) is performed by the embedded AuthService call documented in [Dual-identity authorization](operations/dual-identity-authorization.md), confirmed against the repository's 10.x `SecurityClient` source; applicability to the pinned `8.2023.319.2` package is inferred*.

## Authorization enforcement points

### Attribute-based authorization

| Controller / Action | Attribute | Resource name(s) | Source |
| --- | --- | --- | --- |
| Consumer-defined controllers/actions | `[Authorize(Policy = "<name>")]` (framework attribute; this library does not define a custom attribute) | Consumer-defined, resolved through the named policy | Consumer code |

### Programmatic authorization checks

| Class | Method | Resource / Role | Check type | Source |
| --- | --- | --- | --- | --- |
| `RoleRequirement` | `HandleRequirementAsync` | This instance's own `_role` (self-handling requirement; cross-evaluates every requirement in the policy context) | `IsAuthorized` (via delegation) | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Authorization/RoleRequirement.cs` |
| `NtlmApiAuthorizationService` | `IsAuthorized(requestUser, onBehalfOfUser, role)` | Caller identity, and on-behalf-of identity when present | `IsAuthorized` (delegates to AuthService `ISecurityClient.IsAuthorized`, confirmed against the repository's 10.x source; applicability to the pinned `8.2023.319.2` package is inferred*) | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs` |
| `ConfigureRoleAuthorizationOptions` | `AddPolicies` | Policy factory — builds one `RoleRequirement` per role | Policy construction | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs` |

## Data-level authorization

**None.** No entity-ownership or scope check exists in this library. The `OnBehalfOfUser` claim is the natural input for consumer-side data scoping, but it is unverified (any authenticated caller can name any user) and can be stale if the originating `IHttpJsonClient` is shared across users — it must not be treated as a verified identity for ownership decisions.

## Segregation of duties

**None.** No SOD classes or privilege-combination checks exist in this library.

## Security interfaces and contracts

| Interface / Class | Role | Consumers | Source |
| --- | --- | --- | --- |
| `INtlmApiAuthorizationService` | Dual-identity authorization decision | `RoleRequirement` | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/INtmlApiAuthorizationService.cs` |
| `IOnBehalfOfUserClaimService` | Header→claim conversion | `OnBehalfOfUserClaimMiddleware` | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/IOnBehalfOfUserClaimService.cs` |
| `ICurrentUserService` | Current-user resolution (client side) | `IHttpJsonClientExtensions.ConfigureOnBehalfOfUserAuthorization` | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/ICurrentUserService.cs` |
| `ConfigureRoleAuthorizationOptions` | Abstract policy factory consumers subclass | `IServiceCollectionExtensions.AddNtlmApiAuthorization<T>` | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs` |
| `ISecurityClient` (consumed, `SAIF.NetCore.AuthService`) | Remote role-check client | `NtlmApiAuthorizationService` | `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Interfaces/ISecurityClient.cs` |

## Security findings and risks

Severity scale: **High** (realistic path to acting with another user's authority), **Medium** (weakens a control or enables identity spoofing under plausible conditions), **Low** (fails closed / limited impact), **Informational**.

| # | Finding | Severity | Source |
| --- | --- | --- | --- |
| SEC-NTLM-01 | Cross-user identity leakage via persisted header: a client configured by [Configure an outbound client to send OnBehalfOfUser](operations/configure-outbound-client-header.md) persists the first user's name on every later request if the `IHttpJsonClient` instance outlives one user (e.g. Singleton registration or a cached client). Server-side dual-identity authorization then evaluates a later user's request under an earlier user's name, granting that user's entitlements and misattributing the action.* Repo defaults (Transient `IHttpJsonClient`, Scoped sample typed client) are safe, but nothing in the library prevents or detects an unsafe lifetime. | High | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs` |
| SEC-NTLM-02 | On-behalf-of check is caller-opt-in (fail-open by omission): when the header is absent or blank, [Dual-identity authorization](operations/dual-identity-authorization.md) authorizes on the caller identity alone, so a service account holding the role is authorized for any request it makes without the header — regardless of which end user triggered it.* | Medium | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs` |
| SEC-NTLM-03 | Unverified delegation header (impersonation naming): any authenticated caller may name any user in `OnBehalfOfUser`; there is no allow-list of trusted delegating accounts and no signature/token. Because both identities must pass, the header cannot raise the caller's own privilege, but a user holding role R can act "as" any other R-holder for audit/data-scoping purposes. | Medium | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/OnBehalfOfUserClaimService.cs` |
| SEC-NTLM-04 | Header value reaches the SecurityAPI route unnormalized: the on-behalf-of value is passed raw into the embedded AuthService call, which only replaces `\` with `/` before interpolating it into the URL path. Whether the underlying `IHttpJsonClient`/RestSharp implementation URL-encodes that path segment is unknown.** If it does not, a crafted value containing `/`, `..`, `?`, or `#` could change the path or query evaluated for the on-behalf-of half of the check and return an unintended result; this exploitability has not been tested, only reasoned from the absence of visible encoding in either library.* The caller-identity check is unaffected either way. | Medium | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Authorization/RoleRequirement.cs` |
| SEC-NTLM-05 | `DomainlessUser` normalization defects: the `@` index is computed before the `\` strip, so `SAIF\bob@saif.com` becomes `bob@saif`; short tails can throw `ArgumentOutOfRangeException`. Effect is mostly wrong-name → deny or exception (fails closed). | Low | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/StringExtensions.cs` |
| SEC-NTLM-06 | Middleware-order dependency silently disables the delegated check: the claim is added only if the identity is already authenticated when the middleware runs; under non-IIS hosting with the middleware placed before `UseAuthentication()`, the header would always be ignored.* This has not been exercised against a non-IIS host in this run; the IIS ordering (identity set before the pipeline runs) is the only ordering present in the source-inspected sample configuration, not a recorded host execution.* | Medium (conditional) | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/OnBehalfOfUserClaimService.cs` |
| SEC-NTLM-07 | Multi-role semantics are AND in code but documented as OR in the README, and each self-handling requirement cross-evaluates every role in the policy, so SecurityAPI can be called up to 2·N² times per request for N roles.* Fails closed (more restrictive than documented) but can surprise developers following the README. | Low | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs`, `Authorization/RoleRequirement.cs` |
| SEC-NTLM-08 | Forwarded header lost on retry / duplicate-key exceptions: [Forward the inbound OnBehalfOfUser header](operations/forward-inbound-header.md) does not itself enable persistence, so on a client whose options are otherwise left at their default a retried request can be sent without the header;* calling it alongside [Configure an outbound client to send OnBehalfOfUser](operations/configure-outbound-client-header.md) on the same client throws `ArgumentException`. | Low | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/HttpContextAccessorExtensions.cs` |
| SEC-NTLM-09 | Fail-closed by exception: a `SecurityClientException` from the embedded AuthService call, or a `DomainlessUser` exception, propagates out of `RoleRequirement.HandleRequirementAsync` uncaught; the resulting HTTP response is a function of the host's own error handling and is inferred to be a server error (not a 403) rather than independently verified in this run.* Access is never granted on failure either way. | Low | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Authorization/RoleRequirement.cs` |
| SEC-NTLM-10 | Swagger debug header is documentation-only: hiding the header parameter outside a debugger does not stop the server from honoring the header in any environment. | Informational | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/SwaggerGenOptionsExtensions.cs` |
| SEC-NTLM-11 | A single `INtlmApiAuthorizationService` instance (Transient, but captured by the singleton `AuthorizationOptions`) is reused for the app lifetime; safe only while AuthService registers `ISecurityClient` as a singleton.* No per-request caching exists, so every evaluation re-queries SecurityAPI. | Informational | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs` |
| SEC-NTLM-12 | Dead security surface and version skew: unused `IEnvironmentClient` injection and suffix constants; the public `AuthorizationResource` helper has different semantics from the live AuthService rewrite; AuthService is pinned at `8.2023.319.2` while the suffix-rewrite behavior is verified only against 10.x source.* | Informational | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs`, `Constants.cs` |

### Machine/application authorization

The authenticated caller in the double-hop scenario is typically the calling application's Windows identity (an app-pool service account or machine account). `NtlmApiAuthorizationService` requires this application identity to hold each role itself before the end user is considered — application authorization and user authorization are evaluated against the same role. `DomainlessUser` strips the domain but keeps a trailing `$`, so a machine account is sent to SecurityAPI as `HOST$` with no distinct machine-group logic. Because the service account must hold user roles, it is a high-value principal: any request it makes without the `OnBehalfOfUser` header (SEC-NTLM-02) is authorized for every role it holds.

## Notes
- The embedded AuthService rewrite (`SecurityClient.GetSuffixRole`) only replaces a trailing, case-insensitive `resource` token on a role name with the configured `SecurityClient:resourcesuffix` value (e.g. `XResource` → `XTestResource`); a role name that does not already end in `resource` is left unchanged* — the rewrite does not append `Resource`/`TestResource` to every base name consumers supply.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Service registration](service-registration.md)
- [API reference](api/index.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts. Statements marked with `**` could not be confirmed; treat
> as unknown until verified.

## Sources
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Authorization/RoleRequirement.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/OnBehalfOfUserClaimService.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs`, `Extensions/HttpContextAccessorExtensions.cs`, `Extensions/StringExtensions.cs`, `Extensions/SwaggerGenOptionsExtensions.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Constants.cs`
- `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs`
- `src/SAIF.NetCore.NtlmApiAuth/README.md`
