Skip to content

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.

Authorization architecture

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).

flowchart TD
    Attr["[SecurityApiAuthorize roles]"] --> Policy[SecurityApiPolicy]
    Policy --> Req[SecurityApiRequirement]
    Req --> Handler["SecurityApiHandler (Singleton, OR over roles)"]
    Handler -->|"user = Identity.Name minus DOMAIN\"| Client1[ISecurityClient.IsAuthorized]
    Accessor[IIdentityServiceAccessor] --> Token[TokenIdentityService.IsAuthorized]
    Accessor --> Win[WinIdentityService.IsAuthorized]
    Token -->|"IsInRole(role) == true"| LocalOK[Authorized locally - no remote call]
    Token -->|"IsInRole(role) == false"| Client2["ISecurityClient.IsAuthorized(mailid claim, role)"]
    Win --> Client3["ISecurityClient.IsAuthorized(DOMAIN/user, role)"]
    Client1 --> Suffix["Resource-suffix rewrite (SecurityClient:resourcesuffix)"]
    Client2 --> Suffix
    Client3 --> Suffix
    Suffix --> API["GET {SecurityClient:ServiceUrl}<br>/v1/Users/IsAuthorized/{user}/{role'}"]
    API -->|non-success| Throw[Throw SecurityClientException]

AD resource catalog

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).

Resource name Purpose Enforcement point Source
(none hard-coded) Role names are caller-supplied. SecurityApiAuthorize ctor; SecurityApiHandler.HandleRequirementAsync; TokenIdentityService.IsAuthorized; WinIdentityService.IsAuthorized; SecurityClient.IsAuthorized src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiAuthorize.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/TokenIdentityService.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/WinIdentityService.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs

Resource-name transformation (environment suffix convention)

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)

Name Where Kind
Domain Users AuthTestController.Authorized AuthService integration-test host (not shipped library surface)
Domain Users, Developer_Group, Users AuthController, ValuesController ServiceExtensions integration tests (not shipped library surface)

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.

Authorization enforcement points

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.

Attribute-based authorization

# Element Kind Behavior Source
EP-01 [SecurityApiAuthorize(string roles)] Attribute, derives from AuthorizeAttribute Sets Policy = "SecurityAPI.{roles}"; roles is a comma-separated list; usable on controller or action (BR-AUTH-004) src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiAuthorize.cs
EP-02 SecurityApiPolicy IAuthorizationPolicyProvider, registered Transient 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) src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityAPIPolicy.cs
EP-03 SecurityApiRequirement IAuthorizationRequirement Carries the ProvidedRoles string, read-only after construction. src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiRequirement.cs
EP-04 SecurityApiHandler AuthorizationHandler<SecurityApiRequirement>, registered Singleton 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**. src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs

Decision path: [SecurityApiAuthorize("A,B")] → policy SecurityAPI.A,B → SecurityApiPolicy → SecurityApiRequirement("A,B") → SecurityApiHandler → SecurityClient.IsAuthorized(user, "A"), then "B" → resource-suffix rewrite → GET {ServiceUrl}/v1/Users/IsAuthorized/{user}/{role'}.

Programmatic authorization checks

# Member Resource / Role Check type Behavior Source
EP-05 TokenIdentityService.IsAuthorized(string role) caller-supplied Local-first, remote fallback 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) src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/TokenIdentityService.cs
EP-06 WinIdentityService.IsAuthorized(string role) caller-supplied Always remote SecurityClient.IsAuthorized(UserId, role) where UserId is the full DOMAIN\user (BR-AUTH-014) No local IsInRole check. src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/WinIdentityService.cs
EP-07 BaseIdentityService.IsAuthorized(Predicate<Claim>) caller-supplied predicate Local only Identity.HasClaim(match); no remote call and no resource-suffix rewrite. src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/BaseIdentityService.cs
EP-08 SecurityClient.IsAuthorized(userId, role) caller-supplied Direct remote client call \ → / 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*. src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs
EP-09 MultiIdentityServiceAccessor.IdentityService / IdentityServiceAccessor.IdentityService n/a Identity selection 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. src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/MultiIdentityServiceAccessor.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/IdentityServiceAccessor.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Extensions/AuthenticationServiceExtensions.cs

Enforcement-to-resource mapping:

Enforcement point Resource source Suffix rewrite applied? Decision authority
EP-01→EP-04 (attribute path) Attribute argument (consumer code) Yes (inside SecurityClient) Remote SecurityApi
EP-05 (token) role argument (consumer code) No for the local IsInRole short-circuit; yes for the remote fallback Token role claim, then remote SecurityApi
EP-06 (Windows) role argument (consumer code) Yes Remote SecurityApi
EP-07 (claims predicate) Consumer predicate No Local token/identity claims
EP-08 (direct client) Caller Yes Remote SecurityApi

In-repo consumers of these enforcement points (cross-reference only):

Consumer Uses Source
SAIF.NetCore.NtlmApiAuth → NtlmApiAuthorizationService ISecurityClient.IsAuthorized(requestUser, role) and IsAuthorized(onBehalfOfUser, role) src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs
SAIF.NetCore.ServiceExtensions → RegisterCoreAPIAuthServices/RegisterEdgeAPIAuthServices AddIdentityAccessor().AddDualAuthentication(), AddSecurityApiAuthorizeAttribute() src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs
SAIF.NetCore.ServiceExtensions → AppStartup AddTransient<ISecurityClient, SecurityClient>(), AddIdentityAccessor(), AddSecurityApiAuthorizeAttribute() src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs

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.

Security interfaces and contracts

Interface / Class Role Consumers Source
ISecurityClient Contract for the remote SecurityApi client (role authorization, user profile, group membership, authorization groups, external-user data) SecurityApiHandler, TokenIdentityService, WinIdentityService, SAIF.NetCore.NtlmApiAuth src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Interfaces/ISecurityClient.cs
IAccessTokenClient Contract for an outbound OAuth client-credentials token client Consumer code calling downstream APIs src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Interfaces/IAccessTokenClient.cs
BaseIdentityService Abstract base implementing IIdentityService (ServiceContracts); shared identity/claims surface TokenIdentityService, WinIdentityService src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/BaseIdentityService.cs
IdentityServiceAccessor, MultiIdentityServiceAccessor Implement IIdentityServiceAccessor (ServiceContracts); resolve the active IIdentityService for the current request Consumer code via DI src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/IdentityServiceAccessor.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/MultiIdentityServiceAccessor.cs

Security observations

SEC-01 User-ID inconsistency between paths

Field Value
Observation 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*.
Severity Medium
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/WinIdentityService.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/TokenIdentityService.cs

SEC-02 Domain-agnostic attribute check

Field Value
Observation 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**.
Severity Medium
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs

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.
Severity Low/Medium
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/TokenIdentityService.cs

SEC-04 Fail-closed by exception, not by deny

Field Value
Observation 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.
Severity Low
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs

SEC-05 Policy prefix capture

Field Value
Observation 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.
Severity Low
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityAPIPolicy.cs

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**.
Severity Informational
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityAPIPolicy.cs; src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs

SEC-07 Unencoded route segments

Field Value
Observation 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**.
Severity Low/Medium
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs

SEC-08 Global scheme-selector mutability

Field Value
Observation 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)
Severity Low/Medium
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Extensions/AuthenticationServiceExtensions.cs

SEC-09 Unauthenticated or no-context default

Field Value
Observation 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)
Severity Low
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/MultiIdentityServiceAccessor.cs

SEC-10 No authorization-result caching

Field Value
Observation 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)
Severity Informational
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs

Notes

  • 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.

Sources

  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiAuthorize.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityAPIPolicy.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiRequirement.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClientException.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/OktaTokenClient.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Extensions/AuthenticationServiceExtensions.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/BaseIdentityService.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/TokenIdentityService.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/WinIdentityService.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/IdentityServiceAccessor.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/MultiIdentityServiceAccessor.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Interfaces/ISecurityClient.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Interfaces/IAccessTokenClient.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/IIdentityService.cs
  • src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/Services/NtmlApiAuthorizationService.cs