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).
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, 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 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 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 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 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-insensitiveresourcetoken on a role name with the configuredSecurityClient:resourcesuffixvalue (e.g.XResource→XTestResource); a role name that does not already end inresourceis left unchanged* — the rewrite does not appendResource/TestResourceto every base name consumers supply.
Related documentation¶
- Index
- Overview
- Architecture
- Dependencies
- Operations and processes
- Business rules
- Service registration
- API reference
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.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Options/ConfigureRoleAuthorizationOptions.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/OnBehalfOfUserClaimService.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Extensions/IHttpJsonClientExtensions.cs,Extensions/HttpContextAccessorExtensions.cs,Extensions/StringExtensions.cs,Extensions/SwaggerGenOptionsExtensions.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Constants.cssrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cssrc/SAIF.NetCore.NtlmApiAuth/README.md