This page captures business rules enforced by this library. All eighteen rules below (BR-AUTH-001 through BR-AUTH-018) are enforced by executable code in this library itself; none involve database or SQL operations, and none are inherited from a dependency framework library.
BR-AUTH-001 JWT options are only populated for the Bearer scheme¶
Field
Value
Statement
JwtBearerOptions are only populated when the options framework is configuring the named scheme "Bearer" (JwtBearerDefaults.AuthenticationScheme).
Trigger
The options framework invokes IConfigureNamedOptions<JwtBearerOptions>.Configure(string name, JwtBearerOptions options).
Enforcement
ConfigureJwtBearerOptions.Configure(string, JwtBearerOptions) checks name == JwtBearerDefaults.AuthenticationScheme before reading any oauth setting.
Outcome
Other named JwtBearer schemes are left unconfigured by this class.
BR-AUTH-002 Per-application oauth overrides take precedence¶
Field
Value
Statement
Application-specific oauth keys authority.{ApplicationName} / audience.{ApplicationName} override the generic authority / audience values when present.
Trigger
Options configuration for the "Bearer" scheme, after the generic oauth:authority/oauth:audience values are read.
Enforcement
ConfigureJwtBearerOptions.Configure calls IEnvironmentClient.HasSetting("authority.{ApplicationName}", "oauth") / HasSetting("audience.{ApplicationName}", "oauth") and overwrites options.Authority/options.Audience when each returns true.
Outcome
Per-application authority/audience values win over the generic ones (E4/E5).
BR-AUTH-003 The unnamed Configure overload is a no-op¶
Field
Value
Statement
The unnamed IConfigureOptions<JwtBearerOptions>.Configure(JwtBearerOptions) overload passes Options.DefaultName (an empty string), which never equals "Bearer", so BR-AUTH-001 makes it a no-op.
Trigger
The options framework invokes the unnamed Configure(JwtBearerOptions) overload (required by IConfigureNamedOptions<T>, but not expected to be called in normal use).
Enforcement
ConfigureJwtBearerOptions.Configure(JwtBearerOptions options) => Configure(Options.DefaultName, options); delegates to the named overload with Options.DefaultName.
Outcome
No configuration is applied through this code path; a consumer who registers JwtBearer under a custom scheme name receives no configuration from this class*.
BR-AUTH-005 SecurityAPI-prefixed policy names resolve to a SecurityApiRequirement¶
Field
Value
Statement
Any policy name longer than one character that starts with "SecurityAPI" using the current-culture, case-sensitive string.StartsWith(string) overload is converted to a policy containing only a SecurityApiRequirement, whose roles are the text after the first . (or the whole name if there is no .). All other names fall back to DefaultAuthorizationPolicyProvider.
Trigger
ASP.NET Core authorization middleware calls IAuthorizationPolicyProvider.GetPolicyAsync(policyName).
Enforcement
SecurityApiPolicy.GetPolicyAsync checks policyName.Length > 1 && policyName.StartsWith("SecurityAPI") — the single-argument StartsWith(string) overload, which performs a current-culture string comparison, not an ordinal one* — then builds new AuthorizationPolicyBuilder().AddRequirements(new SecurityApiRequirement(policyName.Substring(policyName.IndexOf(".") + 1))).
Outcome
A matching policy name yields a custom SecurityApiRequirement-only policy; a non-matching name (or the default/fallback policy requests) is delegated to DefaultAuthorizationPolicyProvider.
BR-AUTH-006 An unnamed identity never satisfies a SecurityAPI requirement¶
Field
Value
Statement
A request whose context.User.Identity.Name is null or whitespace-only never satisfies a SecurityApiRequirement. The handler tests only Identity.Name; it never checks Identity.IsAuthenticated, so this rule does not by itself guarantee that every unauthenticated or anonymous caller is denied — a host that assigns a non-empty name to an unauthenticated identity would still pass this guard**.
Trigger
SecurityApiHandler.HandleRequirementAsync evaluates a SecurityApiRequirement.
Enforcement
HandleRequirementAsync returns immediately, without calling context.Succeed, when String.IsNullOrWhiteSpace(context.User.Identity.Name) is true.
Outcome
The requirement is left unsatisfied, so ASP.NET Core authorization denies the request for a null/whitespace name (typically 401/403); whether the host's authentication pipeline can ever produce a non-empty Identity.Name for an unauthenticated principal is a host/policy assumption not verified from this library's source**.
BR-AUTH-007 The handler strips the Windows domain before calling the SecurityApi¶
Field
Value
Statement
SecurityApiHandler strips the Windows domain prefix from Identity.Name by taking the second \-delimited segment (Split("\\")[1]) when the name contains a backslash — not necessarily "everything after the last backslash" — before calling the SecurityApi, unlike WinIdentityService which keeps the domain (SEC-01).
Trigger
SecurityApiHandler.HandleRequirementAsync evaluates a SecurityApiRequirement for a non-empty identity name.
Enforcement
HandleRequirementAsync computes userName = context.User.Identity.Name.Contains("\\") ? context.User.Identity.Name.Split("\\")[1] : context.User.Identity.Name, which takes the second segment of the split array (index 1), not text after the last backslash.
Outcome
The SecurityApi call uses a domain-less user name for a single-backslash DOMAIN\user value, which is different from the full DOMAIN\user value WinIdentityService.IsAuthorized sends for the same principal; a name containing more than one backslash would not behave as "strip everything before the last backslash"*.
BR-AUTH-008 Comma-separated roles use OR semantics¶
Field
Value
Statement
When SecurityApiRequirement.ProvidedRoles contains multiple comma-separated roles, authorization succeeds if the SecurityApi authorizes the user for any one of them; evaluation does not stop at the first success.
Trigger
SecurityApiHandler.HandleRequirementAsync evaluates a SecurityApiRequirement for an identified user.
Enforcement
HandleRequirementAsync loops over requirement.ProvidedRoles.Split(","), awaiting SecurityClient.IsAuthorized(userName, role) for each role and calling context.Succeed(requirement) whenever one returns true; the loop continues after a success (SEC-10).
Outcome
Any one matching role authorizes the request, but every role in the list still triggers a remote call.
After trimming, a role name ending in resource (case-insensitive) has its last occurrence of resource replaced with the configured SecurityClient:resourcesuffix value (for example, hypothetically, Resource → TestResource for a configuration-dependent, illustrative non-production suffix value — the actual per-environment suffix is configuration-defined and not fixed by this library); other role names pass through trimmed and unchanged.
Trigger
SecurityClient.IsAuthorized(userId, role) is called.
Enforcement
SecurityClient.GetSuffixRole(role) trims role, checks role.ToLower().EndsWith("resource"), and if so calls ReplaceLastOccurrence(role, "resource", Suffix) using the SecurityClient:resourcesuffix value cached in the Suffix field.
Outcome
The role name sent to the SecurityApi maps one code-level role name to a different environment-specific AD resource group name, per environment configuration.
BR-AUTH-011 A non-successful SecurityApi response always throws¶
Field
Value
Statement
A non-successful response from any SecurityApi GET call always throws SecurityClientException (with the inner exception attached when one is available); there is no null/false/default fallback.
Trigger
Any SecurityClient method completes its underlying IHttpJsonClient GET call (OP-04 through OP-08, OP-15).
Enforcement
SecurityClient.HandleSecurityServiceGetResponse<T> checks result.IsSuccessful; on failure it throws new SecurityClientException(result.ErrorMessage, result.ErrorException) when ErrorException is not null, otherwise new SecurityClientException(result.ErrorMessage).
Outcome
The exception propagates to the caller (or, for SecurityApiHandler, surfaces as an unhandled exception in the authorization pipeline rather than a 403 — SEC-04).
Returns the matching claim's value; if no claim type contains mailid, FirstOrDefault() returns null and the subsequent .Value access throws NullReferenceException*.
TokenIdentityService.IsAuthorized returns true immediately when this.User.IsInRole(role); only when that check fails does it call await this.SecurityClient.IsAuthorized(this.UserId, role).
Outcome
A token carrying the matching role claim authorizes locally, bypassing both the remote call and the resource-suffix rewrite (SEC-03).
BR-AUTH-014 Windows user ID keeps the domain and is always remote¶
Field
Value
Statement
For WinIdentityService, UserId is the full Identity.Name value (domain kept, e.g. DOMAIN\user), and authorization is always performed remotely with no local short-circuit.
Trigger
Code reads WinIdentityService.UserId or calls WinIdentityService.IsAuthorized(role).
BR-AUTH-015 Authorization header prefix selects the dual authentication scheme¶
Field
Value
Statement
Under the "dual" policy scheme, a request whose Authorization header starts with the literal "Bearer " (case-sensitive, trailing space) is forwarded to the JWT Bearer scheme; every other request is forwarded to the IIS/Windows scheme.
Trigger
The "dual" policy scheme's ForwardDefaultSelector is invoked for an incoming request (registered by AddDualAuthentication).
Enforcement
AuthenticationServiceExtensions.AuthenticationSchemeSelector reads context.Request.Headers["Authorization"].FirstOrDefault() and returns JwtBearerDefaults.AuthenticationScheme when authHeader?.StartsWith("Bearer ") == true, otherwise IISServerDefaults.AuthenticationScheme.
Outcome
The forwarded scheme determines which authentication handler processes the request, and (via BR-AUTH-016) which identity service a consumer later resolves.
BR-AUTH-016 No HttpContext defaults the multi accessor to Windows¶
Field
Value
Statement
When there is no current HttpContext, MultiIdentityServiceAccessor.IdentityService resolves WinIdentityService rather than TokenIdentityService.
Trigger
Code reads MultiIdentityServiceAccessor.IdentityService outside an active HTTP request (for example from background work).
Enforcement
MultiIdentityServiceAccessor.IdentityService computes scheme = this.HttpContextAccessor.HttpContext != null ? AuthenticationSchemeSelector(this.HttpContextAccessor.HttpContext) : IISServerDefaults.AuthenticationScheme, then resolves WinIdentityService for any scheme other than JwtBearerDefaults.AuthenticationScheme.
Outcome
Code running without an HttpContext receives a WinIdentityService whose UserId is null, which throws NullReferenceException on the next SecurityClient.IsAuthorized call (SEC-09).
BR-AUTH-017 Cached Okta token is reused until near expiry¶
Field
Value
Statement
A cached Okta access token is reused until it is within 30 seconds of its recorded expiry; otherwise a new token is requested.
Trigger
Code calls OktaTokenClient.GetToken().
Enforcement
GetToken() checks !this._token.IsValidAndNotExpiring (which is true — i.e. the underlying IsValidAndNotExpiring is false — when AccessToken is empty or ExpiresAt <= DateTime.UtcNow.AddSeconds(30)) before calling GetNewAccessToken().
Outcome
A still-valid token is returned without a network call; an expiring or missing token triggers a synchronous refresh.
BR-AUTH-018 Outbound token requests use HTTP Basic client credentials¶
Field
Value
Statement
A new Okta access token request authenticates with HTTP Basic authentication using base64(ClientId:ClientSecret) and posts a form body of grant_type, scope, and a redirect_uri derived from the current request's scheme and host.
Trigger
OktaTokenClient.GetNewAccessToken() runs, triggered by BR-AUTH-017
Enforcement
GetNewAccessToken sets client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(UTF8.GetBytes($"{ClientId}:{ClientSecret}"))) and POSTs a FormUrlEncodedContent body containing grant_type, scope, and redirect_uri = $"{Request.Scheme}://{Request.Host}" to TokenUrl, then calls response.EnsureSuccessStatusCode().
Outcome
A successful response is deserialized into a new cached OktaToken; a non-2xx response throws from EnsureSuccessStatusCode(), and the call requires an active HttpContext to build redirect_uri or throws NullReferenceException*.
Several rules cross-reference security enforcement points and observations documented in Security and authorization; those links are informational cross-references, not a claim that this library's own code enforces the security page's content.
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.