---
title: SAIF.NetCore.NtlmApiAuth business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.NtlmApiAuth
reference_type: package
---

# SAIF.NetCore.NtlmApiAuth business rules

This page captures business rules enforced by this library, including the rules governing the dual-identity authorization check that delegates part of its decision to `SAIF.NetCore.AuthService`'s `SecurityClient.IsAuthorized`.

## BR-NTLM-001 OnBehalfOfUser header and claim

| Field | Value |
| --- | --- |
| **Statement** | The on-behalf-of user is carried in an HTTP header named `OnBehalfOfUser` and stored as a claim of type `OnBehalfOfUser`; blank values are ignored. |
| **Trigger** | Inbound request / outbound forward |
| **Enforcement** | `OnBehalfOfUserClaimService.SetOnBehalfOfUserClaim`; `AuthorizationHandlerContextExtensions.OnBehalfOfUser`; `HttpContextAccessorExtensions.OnBehalfOfUser` |
| **Outcome** | Claim added / header read |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/OnBehalfOfUserClaimService.cs`, `Constants.cs` |

---

## BR-NTLM-002 Claim only added for authenticated identities

| Field | Value |
| --- | --- |
| **Statement** | The on-behalf-of claim is only added to an authenticated `ClaimsIdentity`; headers on anonymous requests are ignored. |
| **Trigger** | Inbound request |
| **Enforcement** | `OnBehalfOfUserClaimService.SetOnBehalfOfUserClaim` |
| **Outcome** | No claim for anonymous requests |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/OnBehalfOfUserClaimService.cs` |

---

## BR-NTLM-003 Middleware never blocks

| Field | Value |
| --- | --- |
| **Statement** | The claim middleware never blocks a request; enforcement happens only in authorization policies. |
| **Trigger** | Every request |
| **Enforcement** | `OnBehalfOfUserClaimMiddleware.InvokeAsync` |
| **Outcome** | `next()` is always invoked |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Middleware/OnBehalfOfUserClaimMiddleware.cs` |

---

## BR-NTLM-004 Blank request identity is never authorized

| Field | Value |
| --- | --- |
| **Statement** | A request with no (or blank) authenticated identity name is never authorized; SecurityAPI is not called. |
| **Trigger** | Requirement evaluation |
| **Enforcement** | `NtlmApiAuthorizationService.IsAuthorized`; `RoleRequirement.HandleRequirementAsync` |
| **Outcome** | Returns `false` → `context.Fail()` |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs`, `Authorization/RoleRequirement.cs` |

---

## BR-NTLM-005 Request identity must itself be authorized

| Field | Value |
| --- | --- |
| **Statement** | The calling (request) identity must itself be authorized for the role; if it is not, the on-behalf-of user is not evaluated. |
| **Trigger** | Requirement evaluation |
| **Enforcement** | `NtlmApiAuthorizationService.IsAuthorized`, which delegates to AuthService's `SecurityClient.IsAuthorized(userId, role)` — confirmed against the repository's 10.x source; applicability to the pinned `8.2023.319.2` package is inferred* |
| **Outcome** | Returns `false` without evaluating the on-behalf-of user |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs`; `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs` |

---

## BR-NTLM-006 Both identities must pass when on-behalf-of user is present

| Field | Value |
| --- | --- |
| **Statement** | When an on-behalf-of user is present, both identities must be authorized for the role; when absent, the request identity alone suffices. |
| **Trigger** | Requirement evaluation |
| **Enforcement** | `NtlmApiAuthorizationService.IsAuthorized`, which delegates to AuthService's `SecurityClient.IsAuthorized(userId, role)` for each identity — confirmed against the repository's 10.x source; applicability to the pinned `8.2023.319.2` package is inferred* |
| **Outcome** | `true` only if both identities pass |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs`; `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs` |

---

## BR-NTLM-007 Multi-role policies are AND

| Field | Value |
| --- | --- |
| **Statement** | A policy built with `AddPolicies(string policyName, params string[] roles)` contains one requirement per role; every requirement must succeed, and any failing `RoleRequirement` calls `context.Fail()`, so multi-role policies behave as AND, contradicting the README's OR description.* |
| **Trigger** | Policy evaluation |
| **Enforcement** | `ConfigureRoleAuthorizationOptions.AddPolicies`; `RoleRequirement.HandleRequirementAsync` |
| **Outcome** | `context.Fail()` for the authorization requirement unless every per-role `RoleRequirement` succeeds; ASP.NET Core's authorization middleware and the host then translate an overall-failed authorization result into the actual response (typically 403, but this is a framework/host behavior, not something this library sets directly)* |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs`, `Authorization/RoleRequirement.cs` |

---

## BR-NTLM-008 SecurityAPI failures fail closed via exception

| Field | Value |
| --- | --- |
| **Statement** | A SecurityAPI failure is not treated as "not authorized"; `SecurityClientException` propagates out of the handler to the caller. |
| **Trigger** | Remote call failure |
| **Enforcement** | `RoleRequirement.HandleRequirementAsync` (no try/catch); originates in AuthService's `SecurityClient.HandleSecurityServiceGetResponse<T>` — confirmed against the repository's 10.x source; applicability to the pinned `8.2023.319.2` package is inferred* |
| **Outcome** | The uncaught exception propagates out of `HandleRequirementAsync` to whatever invoked authorization (typically ASP.NET Core's authorization middleware); the resulting HTTP response is produced by that caller/host's own exception handling, not by this library* |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Authorization/RoleRequirement.cs`; `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs` |

---

## BR-NTLM-009 Header configuration enables persistence

| Field | Value |
| --- | --- |
| **Statement** | Configuring a client for on-behalf-of authorization enables header persistence, so the user header is sent on every request made by that client instance, and resets other client options to defaults. |
| **Trigger** | Client configuration |
| **Enforcement** | `IHttpJsonClientExtensions.ConfigureOnBehalfOfUserAuthorization` |
| **Outcome** | `PersistHeadersAfterRequest = true` |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs` |

---

## BR-NTLM-010 Header added only when non-blank; duplicate add throws

| Field | Value |
| --- | --- |
| **Statement** | The outbound header is only added when the user name is non-empty/non-blank; adding it twice to the same client throws `ArgumentException`.* |
| **Trigger** | Client configuration / forward |
| **Enforcement** | `ConfigureOnBehalfOfUserAuthorization`; `ForwardOnBehalfOfUserHeader` |
| **Outcome** | Header added, or `ArgumentException` |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs`, `Extensions/HttpContextAccessorExtensions.cs` |

---

## BR-NTLM-011 User name normalization

| Field | Value |
| --- | --- |
| **Statement** | User names are normalized by removing a `DOMAIN\` prefix and an `@domain` suffix (only when the separator index is greater than 1). Applied to the request user on the server and, by default, to the outbound user name on the client. |
| **Trigger** | Name extraction |
| **Enforcement** | `StringExtensions.DomainlessUser`; called from `RoleRequirement`, `MvcCurrentUserService`, `BlazorCurrentUserService` |
| **Outcome** | Short (domainless) name |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/StringExtensions.cs` |

---

## BR-NTLM-012 Policies defined by subclassing ConfigureRoleAuthorizationOptions

| Field | Value |
| --- | --- |
| **Statement** | Policies are defined in code by subclassing `ConfigureRoleAuthorizationOptions` and overriding `SetPolicies`; each role becomes a SecurityAPI-backed requirement. |
| **Trigger** | Startup |
| **Enforcement** | `ConfigureRoleAuthorizationOptions.Configure`/`AddPolicies`; `IServiceCollectionExtensions.AddNtlmApiAuthorization<T>` |
| **Outcome** | Named policies registered |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cs` |

---

## BR-NTLM-013 Current-user source depends on client type

| Field | Value |
| --- | --- |
| **Statement** | The current-user source depends on client type: MVC uses `HttpContext.User`; Blazor uses `AuthenticationStateProvider`. MVC is the default. |
| **Trigger** | Client registration |
| **Enforcement** | `AddNtmlApiAuthorizationClientServices` |
| **Outcome** | `ICurrentUserService` implementation chosen |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IServiceCollectionExtensions.cs` |

---

## BR-NTLM-014 Swagger header only shown when debugging

| Field | Value |
| --- | --- |
| **Statement** | The Swagger `OnBehalfOfUser` header parameter is only added when a debugger is attached at startup. |
| **Trigger** | Swagger configuration |
| **Enforcement** | `SwaggerGenOptionsExtensions.AddDebuggerOnBehalfOfHeader` |
| **Outcome** | Header parameter shown in debug Swagger UI only |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/SwaggerGenOptionsExtensions.cs` |

---

## BR-NTLM-015 AuthorizationResource suffix rewrite — unwired utility

| Field | Value |
| --- | --- |
| **Statement** | A public, unwired utility converts an AD resource group to its environment counterpart by replacing a trailing `resource` with the supplied suffix, or appending the suffix if absent.* |
| **Trigger** | Caller-invoked |
| **Enforcement** | `StringExtensions.AuthorizationResource` |
| **Outcome** | e.g. `SomeGroupTestResource` |
| **Source** | `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/StringExtensions.cs` |

---

## BR-NTLM-016 Environment role-suffix rewrite and domain separator normalization

| Field | Value |
| --- | --- |
| **Statement** | Role names are rewritten for the environment (a role name ending in the literal `resource` has that trailing token replaced with the configured `resourcesuffix` value) and the user's `\` is replaced with `/` before every SecurityAPI call made on behalf of this library. This was confirmed by inspecting the repository's 10.x `SecurityClient` source; applicability to the pinned `8.2023.319.2` package version consumed by this library is inferred*. |
| **Trigger** | Each `ISecurityClient.IsAuthorized` call reached from `NtlmApiAuthorizationService` |
| **Enforcement** | AuthService's `SecurityClient.IsAuthorized` / `GetSuffixRole` |
| **Outcome** | Environment-specific AD group checked |
| **Source** | `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs` |

---

## Notes
- [BR-NTLM-005](#br-ntlm-005-request-identity-must-itself-be-authorized), [BR-NTLM-006](#br-ntlm-006-both-identities-must-pass-when-on-behalf-of-user-is-present), [BR-NTLM-008](#br-ntlm-008-securityapi-failures-fail-closed-via-exception), and [BR-NTLM-016](#br-ntlm-016-environment-role-suffix-rewrite-and-domain-separator-normalization) describe behavior partly implemented in `SAIF.NetCore.AuthService`'s `SecurityClient.IsAuthorized`, reached transitively from `NtlmApiAuthorizationService.IsAuthorized` (see [Dependencies](dependencies.md#api-dependencies) for the full call-chain citation); they are retained here because they determine the outcome of this library's own authorization decision. This same-repository AuthService behavior was confirmed by inspecting the repository's 10.x `SecurityClient` source; its applicability to the `8.2023.319.2` package version actually pinned by this library is inferred*.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.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.

## Sources
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.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/Middleware/OnBehalfOfUserClaimMiddleware.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/OnBehalfOfUserClaimService.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/HttpContextAccessorExtensions.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/StringExtensions.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IServiceCollectionExtensions.cs`
- `src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/SwaggerGenOptionsExtensions.cs`
- `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs`
- `src/SAIF.NetCore.NtlmApiAuth/README.md`
