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

# SAIF.NetCore.OIDC business rules

This page captures business rules enforced by this library.

## BR-OIDC-01 Option configurators apply only to their named scheme

| Field | Value |
| --- | --- |
| **Statement** | Option configurators apply only to the named schemes `"Cookies"` and `"OpenIdConnect"`. |
| **Trigger** | Options build. |
| **Enforcement** | `ConfigureCookieAuthenticationOptions.Configure(string name, CookieAuthenticationOptions options)` L49–92; `ConfigureOpenIdConnectOptions.Configure(string name, OpenIdConnectOptions options)` L50–104. |
| **Outcome** | Other scheme names receive no change. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureCookieAuthenticationOptions.cs; src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |

---

## BR-OIDC-02 Cookie attributes are fixed; Secure policy is not enforced

| Field | Value |
| --- | --- |
| **Statement** | The auth cookie is HttpOnly, Essential, SameSite=Lax, Path "/", has sliding expiration, and expires after `CookieExpireMinutes`. The Secure policy is not enforced. |
| **Trigger** | Cookie options build. |
| **Enforcement** | `ConfigureCookieAuthenticationOptions.Configure`. |
| **Outcome** | Cookie attributes are set as stated; `Cookie.SecurePolicy` defaults to `SameAsRequest`. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureCookieAuthenticationOptions.cs |

---

## BR-OIDC-03 Cookie tickets are protected by a shared file-system key ring

| Field | Value |
| --- | --- |
| **Statement** | Cookie tickets are protected with a file-system key ring at `DPKeyDirectory`, app name `DPApplicationName`, and the key ring is encrypted by the DP certificate when one exists. |
| **Trigger** | Cookie options build. |
| **Enforcement** | `ConfigureCookieAuthenticationOptions.Configure`. |
| **Outcome** | Shared-key cookie across apps with the same app name and directory*. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureCookieAuthenticationOptions.cs |

---

## BR-OIDC-04 OIDC client credentials come from a secure, library-hashed setting

| Field | Value |
| --- | --- |
| **Statement** | OIDC client credentials come from secure setting `NetCore_OIDC.BOLAppCredentials` (hash of `SystemType.Other` + `"SAIF.NetCore.OIDC"`) and must be `clientId\|clientSecret`. |
| **Trigger** | `AppCredentials` get. |
| **Enforcement** | `OidcSettings.AppCredentials`; `OidcAppCredentials..ctor` (ServiceContracts). |
| **Outcome** | Exceptions on a missing or malformed value. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/OidcSettings.cs |

---

## BR-OIDC-05 Inbound JWT claim names are not remapped

| Field | Value |
| --- | --- |
| **Statement** | Inbound JWT claim names are not remapped (the global map is cleared). |
| **Trigger** | OIDC options build. |
| **Enforcement** | `ConfigureOpenIdConnectOptions.Configure`. |
| **Outcome** | Raw claim types (`sub`, `name`) are preserved process-wide. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |

---

## BR-OIDC-06 OIDC uses the authorization-code flow against Okta

| Field | Value |
| --- | --- |
| **Statement** | OIDC uses the authorization-code flow against authority `OktaBaseUrl+OktaAuthorizationServerUrl`, with HTTPS metadata, userinfo claims, saved tokens, and the session lifetime decoupled from token lifetime (`UseTokenLifetime=false`). |
| **Trigger** | OIDC options build. |
| **Enforcement** | `ConfigureOpenIdConnectOptions.Configure`. |
| **Outcome** | Handler configured for the authorization-code flow. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |

---

## BR-OIDC-07 ID tokens must pass full validation

| Field | Value |
| --- | --- |
| **Statement** | ID tokens must be signed, unexpired (clock skew `ClockSkewSeconds`), issued by the authority, and have audience = ClientId. Role claim type = `ClaimTypes.Role`; name claim = `name`. |
| **Trigger** | Token validation. |
| **Enforcement** | `ConfigureOpenIdConnectOptions.Configure` (`TokenValidationParameters`) + Okta `StrictSecurityTokenValidator`. |
| **Outcome** | An invalid token results in a remote failure (OP-04). |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |

---

## BR-OIDC-08 OIDC correlation state uses an unencrypted key ring

| Field | Value |
| --- | --- |
| **Statement** | OIDC correlation/nonce state is protected with the app name `{DPApplicationName}-oidc` in `DPKeyDirectory`, without certificate encryption. |
| **Trigger** | OIDC options build. |
| **Enforcement** | `ConfigureOpenIdConnectOptions.Configure` L93–95. |
| **Outcome** | Unencrypted key ring for OIDC state. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |

---

## BR-OIDC-09 Session/IdP hints and x-idp wildcard parameters are forwarded to Okta

| Field | Value |
| --- | --- |
| **Statement** | `sessionToken`, `idp`, `login_hint` auth properties and any `x-idp-*` query parameters are forwarded to the IdP authorize request. |
| **Trigger** | Challenge. |
| **Enforcement** | `ConfigureOpenIdConnectOptions.BeforeRedirectToIdentityProviderAsync`. |
| **Outcome** | Okta receives the forwarded hints. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |

---

## BR-OIDC-10 Remote auth failures are classified, logged, then redirected

| Field | Value |
| --- | --- |
| **Statement** | Remote auth failures are logged as Warning for correlation/state errors and Error otherwise, then redirected to the login page with `x-idp-err=se`. |
| **Trigger** | `OnRemoteFailure`. |
| **Enforcement** | `ConfigureOpenIdConnectOptions.OnRemoteFailureAsync`. |
| **Outcome** | A friendly redirect is issued; no exception page is shown. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |

---

## BR-OIDC-11 A failed cookie re-authentication aborts the request

| Field | Value |
| --- | --- |
| **Statement** | For authenticated requests, a failed cookie re-authentication aborts the request with `HttpContextAuthenticateException`. |
| **Trigger** | Authenticated request. |
| **Enforcement** | `JwtClaimMiddleware.InvokeAsync`. |
| **Outcome** | An exception is thrown. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/JwtClaimMiddleware.cs |

---

## BR-OIDC-12 JWT claims and the cookie are refreshed on every request

| Field | Value |
| --- | --- |
| **Statement** | `id_token` and `access_token` are mirrored as claims (one per type, replaced on update), and the cookie is re-signed on every authenticated request. |
| **Trigger** | Authenticated request. |
| **Enforcement** | `JwtAuthorizationCookieClaimService.UpdateJwtTokenValues`; `ClaimsPrincipalExtensions.AddUpdateClaim`. |
| **Outcome** | Claims and cookie are refreshed. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtAuthorizationCookieClaimService.cs |

---

## BR-OIDC-13 An expired access token short-circuits the pipeline

| Field | Value |
| --- | --- |
| **Statement** | An expired access token (`ValidTo < UtcNow`, unvalidated decode) triggers a 301 redirect to `RefreshTokenRedirectUrl`, carrying the refresh token and origin in the query. |
| **Trigger** | Authenticated request with refresh middleware enabled. |
| **Enforcement** | `RefreshJwtMiddleware.InvokeAsync`. |
| **Outcome** | The pipeline is short-circuited. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/RefreshJwtMiddleware.cs |

---

## BR-OIDC-14 Data-protection certificate must match subject and validity window

| Field | Value |
| --- | --- |
| **Statement** | The DP certificate is the LocalMachine\My cert whose Subject equals `DPCertName` exactly and is currently within its validity window. |
| **Trigger** | Certificate lookup. |
| **Enforcement** | `DataProtectionCertService.GetCertificate`. |
| **Outcome** | A matching certificate, or none. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/DataProtectionCertService.cs |

---

## BR-OIDC-15 A missing DP certificate is fatal outside developer machines

| Field | Value |
| --- | --- |
| **Statement** | A missing DP certificate is fatal outside developer machines. Developer machines continue without key encryption. |
| **Trigger** | Certificate lookup. |
| **Enforcement** | `DataProtectionCertService.GetCertificate`. |
| **Outcome** | `MissingDataProtectionCertificateException`, or `null` on a developer machine. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/DataProtectionCertService.cs |

---

## BR-OIDC-16 Login redirects always use HTTPS on the current host

| Field | Value |
| --- | --- |
| **Statement** | Login redirects always use HTTPS on the current host: `https://{host}{CookieLoginPath}{CookieSigninUri}`. |
| **Trigger** | Login challenge / remote failure. |
| **Enforcement** | `LoginRedirectService.Redirect`. |
| **Outcome** | A 302 redirect is issued. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/LoginRedirectService.cs |

---

## BR-OIDC-17 The origin query parameter is conditionally added from the already-built login URL

| Field | Value |
| --- | --- |
| **Statement** | The already-constructed login-redirect URL's own query string (not the incoming request's query string) is checked for a non-whitespace `origin` value; `origin = PathBase+Path+QueryString` of the current request is added only when that constructed URL's query lacks `origin` and the current request path does not start with `CookieLoginPath`. No content or safety validation of the `origin` value itself is performed. |
| **Trigger** | Login redirect. |
| **Enforcement** | `LoginRedirectService.Redirect`. |
| **Outcome** | The return URL is preserved in most cases (the freshly-built URL's query is normally empty), without re-adding `origin` when under `CookieLoginPath`. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/LoginRedirectService.cs |

---

## BR-OIDC-18 A role claim is created for every matching, non-array claim

| Field | Value |
| --- | --- |
| **Statement** | A role claim is created for every claim whose lowercased type contains `RoleClaimIdentifier` and whose value contains no `[`. |
| **Trigger** | Claims transformation. |
| **Enforcement** | `RoleClaimService.AddRoleClaims`. |
| **Outcome** | Roles are added to a cloned principal. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimService.cs |

---

## BR-OIDC-19 Role transformation is skipped when roles already exist and are cached

| Field | Value |
| --- | --- |
| **Statement** | Role transformation is skipped when the principal already has a role claim and the subject is cached. |
| **Trigger** | Claims transformation. |
| **Enforcement** | `RoleClaimService.AddRoleClaims`. |
| **Outcome** | The original principal is returned. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimService.cs |

---

## BR-OIDC-20 Array-valued role claims are ignored

| Field | Value |
| --- | --- |
| **Statement** | Array-valued role claims (serialized with `[`) are ignored; only flattened values become roles. |
| **Trigger** | Claims transformation. |
| **Enforcement** | `RoleClaimService.AddRoleClaims`. |
| **Outcome** | No role is added for array-valued claims. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimService.cs |

---

## BR-OIDC-21 Transformed roles are considered fresh for a configurable offset

| Field | Value |
| --- | --- |
| **Statement** | A subject's transformed roles are considered fresh for `RoleTransformCacheOffset` seconds (local time); stale entries are evicted on lookup. |
| **Trigger** | Cache lookup. |
| **Enforcement** | `ClaimTransformationCacheService.Exists`/`Cache`. |
| **Outcome** | `true`/`false`, with stale entries removed. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/ClaimTransformationCacheService.cs |

---

## BR-OIDC-22 All OIDC settings are read from the NetCore_OIDC section

| Field | Value |
| --- | --- |
| **Statement** | All OIDC settings are read from Environment API section `NetCore_OIDC`. Missing string settings resolve to `""`, missing int settings throw. |
| **Trigger** | Property access. |
| **Enforcement** | `OidcSettings`. |
| **Outcome** | A string default, or an exception for missing int settings. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/OidcSettings.cs |

---

## BR-OIDC-23 A token refresh POSTs to a composed Okta endpoint with Basic auth

| Field | Value |
| --- | --- |
| **Statement** | A token refresh POSTs to `{OktaBaseUrl}{OktaAuthorizationServerUrl}{RefreshTokenUrl}{RefreshTokenQueryString}{refreshToken}` with Basic client authentication. |
| **Trigger** | `Refresh` call. |
| **Enforcement** | `JwtRefreshService.Refresh`. |
| **Outcome** | An Okta token response is returned. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtRefreshService.cs |

---

## BR-OIDC-24 A non-successful refresh raises a typed exception

| Field | Value |
| --- | --- |
| **Statement** | A non-successful refresh response raises `JwtRefreshException` carrying the HTTP status. |
| **Trigger** | Refresh failure. |
| **Enforcement** | `JwtRefreshService.Refresh`. |
| **Outcome** | An exception is thrown. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtRefreshService.cs |

---

## BR-OIDC-25 Refreshed expiry is computed from expires_in; rotation is discarded

| Field | Value |
| --- | --- |
| **Statement** | The refreshed `ExpiresAt` = `UtcNow + expires_in` seconds. A rotated refresh token is not returned. |
| **Trigger** | Refresh success. |
| **Enforcement** | `JwtRefreshService.Refresh`. |
| **Outcome** | A `JwtSet` is returned. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtRefreshService.cs |

---

## BR-OIDC-26 Mixed-case URLs are permanently redirected to lower case

| Field | Value |
| --- | --- |
| **Statement** | Mixed-case URLs are permanently (301) redirected to the lower-cased scheme/host/path, preserving the query string. |
| **Trigger** | External-host request. |
| **Enforcement** | `UrlCaseService.EnforceCasing`; `LowerCaseUrlMiddleware.InvokeAsync`. |
| **Outcome** | A 301 is issued and the pipeline continues. |
| **Source** | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/UrlCaseService.cs; src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/LowerCaseUrlMiddleware.cs |

---

## 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.OIDC/src/SAIF.NetCore.OIDC/SAIF.NetCore.OIDC.csproj`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Constants.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Contracts/RefreshTokenResult.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Exceptions/HttpContextAuthenticateException.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Exceptions/JwtRefreshException.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Exceptions/MissingDataProtectionCertificateException.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Extensions/ClaimsPrincipalExtensions.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Extensions/HttpContextExtensions.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/JwtClaimMiddleware.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/LowerCaseUrlMiddleware.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/RefreshJwtMiddleware.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureCookieAuthenticationOptions.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/ClaimTransformationCacheService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/DataProtectionCertService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtAuthorizationCookieClaimService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtRefreshService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/LoginRedirectService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/OidcSettings.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimTransformer.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/UrlCaseService.cs`
