---
title: SAIF.NetCore.AuthService business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: Framework.NCore maintainers
last_reviewed: 2026-10-02
library: SAIF.NetCore.AuthService
reference_type: package
---

# SAIF.NetCore.AuthService business rules

This page captures business rules enforced by this library. All eighteen rules below ([BR-AUTH-001](#br-auth-001-jwt-options-are-only-populated-for-the-bearer-scheme) through [BR-AUTH-018](#br-auth-018-outbound-token-requests-use-http-basic-client-credentials)) 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](configuration.md#environment-api-settings)). |
| **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](business-rules.md#br-auth-001-jwt-options-are-only-populated-for-the-bearer-scheme) 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](security-authorization.md#sec-01-user-id-inconsistency-between-paths)). |
| **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](security-authorization.md#sec-10-no-authorization-result-caching)). |
| **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](operations/check-role-authorization-remotely.md) through [OP-08](operations/get-external-user.md), [OP-15](operations/handle-a-security-api-response.md)). |
| **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](security-authorization.md#sec-04-fail-closed-by-exception%2C-not-by-deny)). |
| **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](security-authorization.md#sec-03-token-role-claims-bypass-the-remote-check-and-the-suffix-rewrite)). |
| **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](business-rules.md#br-auth-009-backslashes-in-user-ids-are-rewritten-to-forward-slashes) 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](business-rules.md#br-auth-016-no-httpcontext-defaults-the-multi-accessor-to-windows)) 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](security-authorization.md#sec-09-unauthenticated-or-no-context-default)). |
| **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](business-rules.md#br-auth-017-cached-okta-token-is-reused-until-near-expiry) |
| **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](security-authorization.md); those links are informational cross-references, not a claim that this library's own code enforces the security page's content.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.md)
- [Service registration](service-registration.md)
- [API reference](api/index.md)

> **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`
