Skip to content

SAIF.NetCore.AuthService business rules

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.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/ConfigureJwtBearerOptions.cs

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).
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/ConfigureJwtBearerOptions.cs

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*.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/ConfigureJwtBearerOptions.cs

BR-AUTH-004 SecurityApiAuthorize maps roles to a dynamic policy name

Field Value
Statement [SecurityApiAuthorize(roles)] sets the attribute's Policy to "SecurityAPI." + roles, where roles is a comma-separated string supplied by the consumer.
Trigger A consumer applies [SecurityApiAuthorize("RoleA,RoleB")] to a controller or action.
Enforcement SecurityApiAuthorize's constructor assigns Policy = $"SecurityAPI.{roles}" via the base AuthorizeAttribute.Policy property.
Outcome ASP.NET Core authorization middleware requests the dynamic policy name "SecurityAPI.RoleA,RoleB" from the registered IAuthorizationPolicyProvider.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiAuthorize.cs

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.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityAPIPolicy.cs

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

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"*.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs

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.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs

BR-AUTH-009 Backslashes in user IDs are rewritten to forward slashes

Field Value
Statement A \ character in a user ID is replaced with / before the ID is used to build a SecurityApi URL path.
Trigger SecurityClient.IsAuthorized or SecurityClient.GetUser<T> is called with a user ID that may contain a domain prefix (e.g. DOMAIN\user).
Enforcement Both methods call userId = userId.Replace("\\", "/") before composing the request path.
Outcome The resulting URL path segments are {domain}/{user} rather than containing a literal backslash, which is not URL-safe.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs

BR-AUTH-010 Environment resource suffix rewrite

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

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).
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/SecurityClient.cs

BR-AUTH-012 Token user ID comes from the mailid claim

Field Value
Statement For TokenIdentityService, UserId is the value of the first claim whose type contains mailid (case-insensitive).
Trigger Code reads TokenIdentityService.UserId.
Enforcement TokenIdentityService.UserId evaluates this.Claims.Where(claim => claim.Type.ToLower().Contains("mailid")).FirstOrDefault().Value.
Outcome Returns the matching claim's value; if no claim type contains mailid, FirstOrDefault() returns null and the subsequent .Value access throws NullReferenceException*.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/TokenIdentityService.cs

BR-AUTH-013 Token users can authorize locally via a role claim

Field Value
Statement For TokenIdentityService, a matching local role claim (User.IsInRole(role)) authorizes the request without calling the remote SecurityApi.
Trigger Code calls TokenIdentityService.IsAuthorized(role).
Enforcement 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).
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/TokenIdentityService.cs

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).
Enforcement WinIdentityService.UserId returns this.Identity?.Name unmodified; WinIdentityService.IsAuthorized unconditionally calls await SecurityClient.IsAuthorized(this.UserId, role).
Outcome Every authorization check for a Windows-authenticated user reaches the remote SecurityApi (BR-AUTH-009 rewrites the domain separator for the URL).
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/WinIdentityService.cs

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.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Extensions/AuthenticationServiceExtensions.cs

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).
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/MultiIdentityServiceAccessor.cs

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.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/OktaTokenClient.cs

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*.
Source src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/OktaTokenClient.cs

Notes

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

Sources

  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/ConfigureJwtBearerOptions.cs
  • 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/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/Extensions/AuthenticationServiceExtensions.cs