SAIF.NetCore.AuthService security and authorization¶
This page documents authorization mechanisms implemented or consumed by this library. Authentication is wired by this library (JWT Bearer / IIS Windows / dual scheme selection) but is delegated entirely to ASP.NET Core authentication handlers; this page records authentication only where it determines the identity value fed into an authorization decision. The library itself defines no controllers, endpoints, or [Authorize]/[AllowAnonymous] usages — it is a mechanism provider that a consuming application must register and apply.
Role and resource decisions are delegated to a remote REST "SecurityApi" via ISecurityClient.IsAuthorized, reached either through the declarative [SecurityApiAuthorize] pipeline or directly by consumer code; TokenIdentityService additionally accepts a matching token role claim as a local shortcut before falling back to the remote call, and IsAuthorized(Predicate<Claim>) is purely local with no remote call at all. There is no WCF SecurityService integration, no direct AD/LDAP access, no data-level authorization, and no segregation-of-duties enforcement in this library (BR-AUTH-004 through BR-AUTH-011).
No AD group or resource-name literals exist in the library code — every role/resource name is supplied by the caller at run time, either as the [SecurityApiAuthorize(roles)] argument or as the role parameter of IIdentityService.IsAuthorized(string)/ISecurityClient.IsAuthorized(userId, role).
The library implements the SAIF Resource/TestResource environment-suffix convention on the client side, before calling the SecurityApi: a trimmed role ending in resource (case-insensitive) has the last occurrence of resource replaced with the configured SecurityClient:resourcesuffix value; roles not ending in resource pass through unchanged (BR-AUTH-010). This transformation replaces a trailing resource token rather than appending a suffix to a base name — consumers must pass the full production resource name (for example Resource, not a shortened form), and the library substitutes the configured suffix for that trailing token. The per-environment resourcesuffix value itself is environment configuration and is not present in this repository**.
AD group names seen outside the library (consumers / test hosts — not library behavior)¶
No direct AD/LDAP access exists in this library — no System.DirectoryServices/System.DirectoryServices.AccountManagement usage, no PrincipalContext/GroupPrincipal/DirectoryEntry/DirectorySearcher, and no LDAP strings. All AD questions are delegated to the remote SecurityApi over HTTP through ISecurityClient's five operations: IsAuthorized, GetUser/GetUser<T>, GetUsersByGroup, GetAuthorizationGroups, and GetExternalUser; only IsAuthorized makes an authorization decision inside the library.
Required registrations for the attribute-based enforcement points (EP-01 through EP-04) to take effect are services.AddSecurityApiAuthorizeAttribute() plus AddAuthorization() in the consumer, and an identity registration (AddTokenIdentityService/AddWinIdentityService/AddIdentityAccessor). The programmatic checks (EP-05 through EP-08) call TokenIdentityService/WinIdentityService/SecurityClient members directly and do not require AddSecurityApiAuthorizeAttribute() or the declarative attribute/policy pipeline — only an identity or ISecurityClient registration.
For a policy name Length > 1 starting with SecurityAPI, builds a policy containing only SecurityApiRequirement(roles); no RequireAuthenticatedUser(), no authentication schemes added; all other names delegate to DefaultAuthorizationPolicyProvider (BR-AUTH-005)
Returns without succeeding if Identity.Name is null/whitespace — an empty-name guard, not a check of Identity.IsAuthenticated (BR-AUTH-006); strips the domain from Identity.Name by taking the second \-delimited segment (BR-AUTH-007); for each role in ProvidedRoles calls ISecurityClient.IsAuthorized, succeeding on any true with no short-circuit (BR-AUTH-008); never calls context.Fail(). Authentication itself is performed by the host's ASP.NET Core authentication handler, not by this handler; whether a host can ever present a non-empty Identity.Name for an unauthenticated principal is a host/policy assumption not verified here**.
Local: User.IsInRole(role) returns true with no remote call. Remote fallback: SecurityClient.IsAuthorized(UserId, role) where UserId is the mailid claim value (BR-AUTH-013)
\ → / on the user ID; resource-suffix rewrite; GET to /v1/Users/IsAuthorized/{userId}/{role}; returns the deserialized bool; a non-success response throws SecurityClientException — no default-deny false (BR-AUTH-009, BR-AUTH-011). Used directly by EP-04/05/06 and by external consumers such as SAIF.NetCore.NtlmApiAuth*.
Determines which of EP-05/EP-06 (and therefore which user-ID rule) applies. Multi: AuthenticationSchemeSelector(HttpContext) — a Bearer header resolves TokenIdentityService, anything else resolves WinIdentityService; with no HttpContext, resolves WinIdentityService (BR-AUTH-016) Single: whatever IIdentityService is registered.
AppStartup registers ISecurityClient as Transient, while SecurityApiHandler is a library-registered Singleton — the Singleton handler therefore captures one SecurityClient instance for the process lifetime; this is believed harmless since SecurityClient is stateless after construction*, but it is a lifetime mismatch.
User-ID inconsistency between paths: the attribute path (EP-04) strips the domain from Identity.Name, WinIdentityService (EP-06) sends the full DOMAIN\user, and TokenIdentityService (EP-05) sends the mailid claim value — the same principal can reach the SecurityApi under different identifiers depending on the path; for JWT callers on the attribute path, Identity.Name depends on the JwtBearer NameClaimType mapping rather than mailid, so it may be empty (a deny) or a different value*.
Domain-agnostic attribute check: stripping the domain makes DOMAINA\bob and DOMAINB\bob indistinguishable on the attribute path; whether this matters depends on how the SecurityApi resolves a domain-less user**.
SEC-03 Token role claims bypass the remote check and the suffix rewrite¶
Field
Value
Observation
Token role claims bypass the remote check and the suffix rewrite: a matching ClaimTypes.Role claim grants access locally in TokenIdentityService.IsAuthorized, using the raw caller role string rather than the environment-suffixed one — trust in this path equals trust in the token issuer's role claims.
Fail-closed by exception, not by deny: a SecurityApi error throws SecurityClientException from inside the authorization handler or identity service, so the request fails with an unhandled exception (typically HTTP 500) rather than a 403 — access is not granted, but the failure mode differs from a normal deny.
Policy prefix capture: any policy name starting with SecurityAPI (current-culture, case-sensitive comparison via the single-argument StartsWith(string) overload, not ordinal) is intercepted; a name without . uses the whole policy name as the role string.
SEC-06 No explicit authenticated-user requirement in built policies¶
Field
Value
Observation
No explicit authenticated-user requirement in built policies: SecurityApiPolicy builds a policy with only SecurityApiRequirement, and the generated policy never calls RequireAuthenticatedUser(). SecurityApiHandler (EP-04) only guards against a null/whitespace Identity.Name; it never inspects Identity.IsAuthenticated. This blocks requests with no name at all, but it does not by itself guarantee that every unauthenticated or anonymous caller is denied — that depends on whether the host's authentication pipeline can ever populate Identity.Name for an identity that is not authenticated, which is a host/policy assumption not verified from this library's source**.
Unencoded route segments: user IDs, roles, domains, and group names are interpolated into URL paths with no URL encoding beyond \ → /; roles containing spaces (e.g. Domain Users), /, ?, or # change the request path or query sent to the SecurityApi; whether IHttpJsonClient encodes paths is outside this library**.
Global scheme-selector mutability: AuthenticationSchemeSelector is a public static mutable field — reassigning it changes which identity service, and therefore which user-ID rule (EP-09), applies process-wide (BR-AUTH-015)
Unauthenticated / no-context default: with no HttpContext, MultiIdentityServiceAccessor returns WinIdentityService whose UserId is then null, and SecurityClient.IsAuthorized calls userId.Replace("\\", "/") on that null, which throws NullReferenceException* (BR-AUTH-016)
No authorization-result caching: each attribute evaluation makes one remote call per listed role without stopping after the first success, and there is no per-request or cross-request cache (BR-AUTH-008)
A trailing resource token in a role name is replaced with the configured SecurityClient:resourcesuffix value at runtime; Resource and TestResource below are illustrative examples of that convention, not verified per-track values.
No WCF SecurityService integration, direct AD/LDAP access, data-level authorization, segregation-of-duties enforcement, or machine/application AD authorization exists in this library; the outbound OktaTokenClient/IAccessTokenClient capability is application credentialing toward downstream services, not an inbound authorization check.
Open items not resolvable from this repository: how the remote SecurityApi resolves a domain-less user and which AD domain it searches by default; the per-environment SecurityClient:resourcesuffix values; how IHttpJsonClient authenticates to the SecurityApi and whether it URL-encodes path segments; whether the domain stripping in SecurityApiHandler is intentional given WinIdentityService keeps the domain; whether the SecurityApi is backed by the same AD lookups as the BOF WCF SecurityService.
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.