---
title: SAIF.NetCore.OIDC security and authorization
summary: Authentication architecture, security findings, and authentication/session enforcement gates this library provides.
status: draft
owner: SAIF Framework Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.OIDC
reference_type: package
---

# SAIF.NetCore.OIDC security and authorization

This library is a **framework authentication provider**, not a consumer of infrastructure-established identity. Unlike internal BOF applications, which only receive a `WindowsIdentity` already established by NTLM/AD, this library implements the entire external-host authentication pipeline: the Okta OpenID Connect authorization-code flow, the shared encrypted authentication cookie, data-protection key management, JWT persistence and refresh, and the (currently unwired) mapping of Okta claims to `ClaimTypes.Role`. It enforces no application resource authorization of its own; that is delegated to `SAIF.NetCore.AuthService`'s `SecurityApiHandler`/`ISecurityClient.IsAuthorized`, composed by `SAIF.NetCore.ServiceExtensions`'s `AppStartup`.

In the inspected `AppStartup` composition, this library is active only when the host runs with `ASPNETCORE_HOST_LOCATION=EXTERNAL`: `AppStartup.ConfigureServices` then calls `RegisterOidcServices`, and `AppStartup.Configure` wires the OIDC middleware. `IServiceCollectionExtensions.RegisterOidcServices` itself carries no `HostLocation` guard and the public OIDC types can be registered directly by any consumer composition, so whether every internal host actually avoids loading this pipeline, and whether any other composition besides `AppStartup` exists, are unknown**.

## Authorization architecture

The library establishes the principal through an Okta OIDC authorization-code challenge, persists it in a shared, encrypted `HttpOnly` cookie, re-validates it on every request, and redirects to a dedicated refresh application when the access token has expired. It intends to add Okta group claims as `ClaimTypes.Role`, but the transformer that does so is never invoked because it is registered only as a concrete type (see [SEC-OIDC-03](#sec-oidc-03-roleclaimtransformer-not-registered-as-iclaimstransformation-%E2%80%94-medium)). Resource authorization on external hosts is performed by `SecurityApiHandler`, outside this library; this library's only contribution to that check is mapping the OIDC `name` claim to `Identity.Name`, used as the AD resource-check user ID.

```mermaid
flowchart TD
    Req[Unauthenticated request] --> Cookie{Cookie auth challenge}
    Cookie -->|No valid cookie| G01[G-01 Cookie challenge to login redirect]
    G01 --> OIDC[OIDC authorization-code challenge to Okta]
    OIDC --> G03[G-03 OIDC token validation]
    G03 -->|Invalid| G04[G-04 OIDC remote failure handling]
    G04 --> G01
    G03 -->|Valid| SignIn[SignInAsync writes shared cookie]
    SignIn --> Authed[Authenticated request]
    Authed --> G06[G-06 Cookie re-authentication and claim sync]
    G06 -->|Fails| Exc[HttpContextAuthenticateException]
    G06 -->|Succeeds| G05[G-05 Access-token expiry gate]
    G05 -->|Expired| Refresh[301 to JwtRefresh app - request ends]
    G05 -->|Valid| Routing[UseRouting / UseAuthorization]
    Routing --> SecApi[SecurityApiHandler.IsAuthorized - AuthService]
    SecApi -->|Deny| G02[G-02 Cookie forbid to access-denied path]

    subgraph NotWired["Not wired into the active pipeline<br>unless a consumer registers RoleClaimTransformer<br>as IClaimsTransformation"]
        G07[G-07 Role-claim shaping - not wired]
    end
    G06 -.->|"only if registered -<br>not observed in any analyzed registration path"| G07
```

## AD resource catalog

**Not applicable.** This library references no AD resource names, AD group names, LDAP paths, or `*Resource`/`*TestResource` literals. It is not a `System.DirectoryServices`/`System.DirectoryServices.AccountManagement` consumer, does not call the WCF SecurityService, and defines no `ISecurityManager` or `[SAIFAuthorize]` usage.

## Authorization enforcement points

### Attribute-based authorization

**Not applicable.** No `[Authorize]`, `[AllowAnonymous]`, `[SaifAuthorize]`, or policy attribute appears in this library.

### Programmatic authorization checks

**Not applicable.** No `IsInRole`, `IsAuthorized`, `HasAccess`, `CheckAccess`, `CheckClaim`, or `IAuthorizationService` call appears in this library. Resource authorization on external hosts is performed by `SAIF.NetCore.AuthService`'s `SecurityApiHandler.HandleRequirementAsync`, which calls `ISecurityClient.IsAuthorized(userName, role)` per comma-separated role; that type lives outside this library.

### Authentication and session gates provided by this library

Since this library's real security surface is authentication and session management rather than resource authorization, its enforcement points are documented as gates rather than as authorization checks.

| ID | Gate | Trigger | Behavior | Effect | Source |
| --- | --- | --- | --- | --- | --- |
| G-01 | Cookie challenge → login redirect | Unauthenticated request that hits an authorization requirement (Cookie challenge scheme) | `OnRedirectToLogin` → `LoginRedirectService.Redirect` → `https://{Host}{CookieLoginPath}{CookieSigninUri}?origin={PathBase}{Path}{QueryString}` | Deny (redirect) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureCookieAuthenticationOptions.cs; src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/LoginRedirectService.cs |
| G-02 | Cookie forbid → access-denied path | Authenticated request that fails authorization | Redirect to `CookieAccessDeniedPath` | Deny | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureCookieAuthenticationOptions.cs |
| G-03 | OIDC token validation | Okta authorization-code callback (`CallbackPath` `/authorization-code/callback`) | Validates signature, issuer, audience, and lifetime, then issues the cookie (`SaveTokens = true`) | Deny → G-04 | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |
| G-04 | OIDC remote failure | Correlation, state, or any other callback failure | Log, then `HandleResponse`, then redirect to login with `x-idp-err=se` | Deny (redirect) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs |
| G-06 | Cookie re-authentication and claim sync | Every authenticated request (runs before G-05 in the pipeline) | Re-authenticates the cookie and rewrites JWT claims; throws on authentication failure | Deny (exception) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/JwtClaimMiddleware.cs |
| G-05 | Access-token expiry gate | Authenticated request (after G-06) whose access token's `exp` has passed | 301 to the refresh app; the request is not processed further — this ends the request ([SEC-OIDC-01](#sec-oidc-01-refresh-token-in-a-301-redirect-url-%E2%80%94-high%2C-verified)) | Deny (redirect) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/RefreshJwtMiddleware.cs |
| G-07 | Role-claim shaping | (Intended) every authentication | Promotes Okta role claims to `ClaimTypes.Role`. Not wired** ([SEC-OIDC-03](#sec-oidc-03-roleclaimtransformer-not-registered-as-iclaimstransformation-%E2%80%94-medium)); latent defect [SEC-OIDC-02](#sec-oidc-02-role-claim-key-typo-makes-every-claim-a-role-%E2%80%94-high-latent%3B-critical-if-activated) | — (not executed) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimTransformer.cs |
| G-08 | Data-protection certificate presence | Cookie options configuration | Throws `MissingDataProtectionCertificateException` on servers without the certificate | Deny (startup/config) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/DataProtectionCertService.cs |

The middleware composed by `AppStartup` runs, in order: `UseAuthentication` → `LowerCaseUrlMiddleware` (OpenIdConnect challenge type only) → `JwtClaimMiddleware` (G-06) → `RefreshJwtMiddleware` (G-05; only when `UseJwtRefreshMiddleware`) → `UseRouting` → `UseAuthorization`. G-06 and G-05 therefore both run before routing and authorization, on every authenticated request including static files served after them — and in that order, with claim synchronization (G-06) always preceding the expiry gate (G-05), not the reverse.

## Data-level authorization

**Not applicable.** The library performs no entity-ownership or composite (identity plus resource) checks.

## Segregation of duties

**Not applicable.** No segregation-of-duties checks were found.

## Machine/application authorization

**No machine-account AD authorization.** The library authenticates itself as an application in two places: to Okta as an OIDC confidential client (client ID/secret from the secure setting `BOLAppCredentials`), and to the Okta token endpoint using HTTP Basic for refresh. The secure-setting retrieval uses a system hash (`ISystemHashService.ComputeHash(SystemType.Other, "SAIF.NetCore.OIDC")`), which is a caller-identity scheme for configuration access, not for user authorization. See [Configuration](configuration.md).

## Security interfaces and contracts

| Interface / Class | Role | Consumers | Source |
| --- | --- | --- | --- |
| `IOidcSettings` | Typed facade over the `NetCore_OIDC` Environment API section; sole implementation `OidcSettings` | Every service and option configurator in this library | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/OidcSettings.cs; src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/IOidcSettings.cs |
| `IDataProtectionCertService` | Resolves the data-protection certificate used to encrypt the cookie key ring | `ConfigureCookieAuthenticationOptions` | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/DataProtectionCertService.cs |
| `IJwtAuthorizationCookieClaimService` | Synchronizes JWT values into claims and re-signs the authentication cookie | `JwtClaimMiddleware` | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtAuthorizationCookieClaimService.cs |
| `IJwtRefreshService` | Exchanges a refresh token for a new access/ID token against Okta | External JwtRefresh application* (identity inferred from the redirect target naming; the verified fact is only that no in-repo caller exists) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/JwtRefreshService.cs |
| `IRoleClaimService` / `IClaimTransformCacheService` | Promotes Okta claims to `ClaimTypes.Role` and caches transformed subjects | `RoleClaimTransformer` (not registered as `IClaimsTransformation`) | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimService.cs; src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/ClaimTransformationCacheService.cs |
| `ILoginRedirectService` | Builds and issues the login redirect | Cookie `OnRedirectToLogin`; OIDC `OnRemoteFailure` | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/LoginRedirectService.cs |
| `IUrlCaseService` | Detects and redirects mixed-case URLs to lower case | `LowerCaseUrlMiddleware` | src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/UrlCaseService.cs |

## Security findings

| ID | Title | Severity | Status |
| --- | --- | --- | --- |
| SEC-OIDC-01 | Refresh token exposed in the query string of a cacheable 301 redirect | High | Active on every expired-session request |
| SEC-OIDC-02 | `RoleClaimIdentitifier` key typo resolves to `""`, so every claim would become a role | High (latent); Critical if SEC-OIDC-03 is fixed alone | Latent (gated by SEC-OIDC-03) |
| SEC-OIDC-03 | `RoleClaimTransformer` is not registered as `IClaimsTransformation`, so role mapping never runs | Medium | Active |
| SEC-OIDC-04 | Login redirect URL is built from the request `Host` header (host-header-driven redirect) | Medium | Active |
| SEC-OIDC-05 | Refresh token sent to Okta in the request URL, not the form body | Medium | Active when consumers call `JwtRefreshService` |
| SEC-OIDC-06 | `Identity.Name` is mapped to the OIDC `name` claim and used as the user ID for AD resource checks | Medium | Active (cross-library) |
| SEC-OIDC-07 | Data-protection key ring can be stored unencrypted (dev-machine bypass; OIDC handler ring never certificate-protected) | Low–Medium | Active |
| SEC-OIDC-08 | Arbitrary `x-idp-*` request query parameters are forwarded to the Okta authorize request | Low | Active (OpenIdConnect challenge apps) |
| SEC-OIDC-09 | Raw ID/access JWTs are duplicated into identity claims | Low | Active |
| SEC-OIDC-10 | Singleton role-transform cache uses a non-thread-safe `Dictionary` with a nullable key | Low | Latent (gated by SEC-OIDC-03) |
| SEC-OIDC-11 | Cookie `SecurePolicy` is not set explicitly; HTTPS redirect can be disabled by environment variable | Low | Active |
| SEC-OIDC-12 | `HttpContextExtensions.RewriteAuthCookie` assigns the access token to the wrong variable | Informational | Dead code (no callers in repo) |

### SEC-OIDC-01 Refresh token in a 301 redirect URL — High, verified

`RefreshJwtMiddleware.RedirectToRefreshTokenApp` reads the Okta refresh token from the authentication properties, sets `StatusCode = 301`, and builds `Location = "{RefreshTokenRedirectUrl}?refreshToken={refreshToken}&origin={PathBase+Path+QueryString}"` — neither value URL-encoded. The redirect fires whenever the cookie-held access token's `ValidTo < DateTime.UtcNow`, and `next` is not called in that case.

The default scope list includes `offline_access`, so the Okta refresh token is a long-lived bearer credential that, together with the client credentials, can mint new access and ID tokens. Until this redirect, it lived only inside the encrypted, `HttpOnly` authentication cookie. Exposure channels: browser history/address bar, server/proxy access logs (IIS W3C logs record `cs-uri-query` by default*), permanent-redirect caching (a 301 is heuristically cacheable per RFC 9110 §15.4.2*), Referer leakage (depends on the refresh app**), and parameter injection via the unencoded `origin` value (depends on the refresh app's binder*).

Severity is High rather than Critical because exploitation requires access to logs, history, or a shared device; the relative redirect reduces (but does not eliminate) the need for a network-adjacent attacker, and the extent to which the inspected deployment actually enforces HTTPS end-to-end is unknown** (see [SEC-OIDC-11](#sec-oidc-11-cookie-transport-policy-implicit-(low)); `ASPNETCORE_REDIRECT_SSL` can disable the host's HTTPS redirection). Remediation direction: keep the refresh token server-side (e.g. have the refresh app read it from the shared cookie), use 302/307 instead of 301, and URL-encode/validate `origin` as a local path.

### SEC-OIDC-02 Role-claim key typo makes every claim a role — High latent; Critical if activated

The lookup constant `RoleClaimIdentitifierKey = "RoleClaimIdentitifier"` is misspelled (extra "it"). `OidcSettings.RoleClaimIdentifier` reads that misspelled key from section `NetCore_OIDC`. The seed data defines only the **correctly** spelled key `RoleClaimIdentifier` = `_roles_`. A missing key resolves to `""` with no exception and no log entry. `RoleClaimService.AddRoleClaims` then selects claims where `x.Type.ToLower().Contains(roleClaimType) && !x.Value.Contains("[")`, and `"anything".Contains("")` is always `true` in .NET, so **every** non-array claim — `sub`, `name`, `email`, `preferred_username`, and the `id_token`/`access_token` claims added by [SEC-OIDC-09](#sec-oidc-09-raw-jwts-stored-as-identity-claims-%E2%80%94-low) — becomes a `ClaimTypes.Role` claim.

Any `[Authorize(Roles = "X")]`, `User.IsInRole("X")`, or `RequireRole("X")` check then succeeds whenever a user can make any claim value equal `X`. If a profile attribute is user-editable in Okta self-service, a user can grant themselves an arbitrary role — a privilege escalation*. Okta profile editability is tenant configuration**. The defect is latent today only because the transformer is never invoked ([SEC-OIDC-03](#sec-oidc-03-roleclaimtransformer-not-registered-as-iclaimstransformation-%E2%80%94-medium)); it becomes Critical the moment that registration gap is fixed without also fixing this key, so the two defects must be remediated together, key first. Status is unknown if the production Environment API happens to store the misspelled key (open question [Q-S01](#open-questions)).

Secondary weakness (verified): even with the correct pattern `_roles_`, the match is a case-insensitive substring match on the claim type (the type is lowercased, but the pattern is not), so any IdP claim whose type contains `_roles_` is promoted, and the `!Value.Contains("[")` filter also drops any legitimate role whose name contains `[`.

### SEC-OIDC-03 RoleClaimTransformer not registered as IClaimsTransformation — Medium

`RoleClaimTransformer : IClaimsTransformation`'s XML doc says it "will be called automatically by the authentication middleware", but registration is `services.AddScoped<RoleClaimTransformer>()` — the concrete type only. No active `AddScoped/AddTransient<IClaimsTransformation, RoleClaimTransformer>` registration for this type exists in the repository. `AddAuthentication()` registers the default `NoopClaimsTransformation`, which `AuthenticationService.AuthenticateAsync` resolves instead*. `RoleClaimTransformer.TransformAsync` is therefore never called, and principals carry no `ClaimTypes.Role` claims derived from Okta `*_roles_*` claims.

Consequence: role-based checks on external hosts fail closed (`IsInRole` returns false; `[Authorize(Roles = "X")]` returns 403 to `CookieAccessDeniedPath`), the documented Okta-group-to-role authorization model is not in effect, and in practice external-host authorization goes through `SecurityApiPolicy`/`SecurityApiHandler`, which does not use role claims. This is Medium rather than High because the failure direction is deny, but it is the gate that keeps [SEC-OIDC-02](#sec-oidc-02-role-claim-key-typo-makes-every-claim-a-role-%E2%80%94-high-latent%3B-critical-if-activated) latent. Consumer applications outside this repository might register the transformer themselves**.

### SEC-OIDC-04 Host-header-driven login redirect (Medium)

`LoginRedirectService.Redirect` builds an absolute URL, `new UriBuilder($"https://{context.Request.Host}{CookieLoginPath}{CookieSigninUri}")`, then calls `Response.Redirect`. This runs from the cookie `OnRedirectToLogin` event and the OIDC `OnRemoteFailure` event. If the IIS site binding or ASP.NET Core `AllowedHosts` accepts arbitrary `Host` values, a crafted `Host` header produces a redirect to an attacker-controlled host*; the realistic attack path is poisoning a shared cache keyed on URL but not `Host`. Host filtering is not configured in this library, and the site binding is outside the repository**. `origin` itself is set from `PathBase + Path + QueryString`, always a local path, so this library does not create an open redirect through `origin` — validating `origin` before use is the login app's responsibility**. The full redirect URL, including query string, is logged at Info level.

### SEC-OIDC-05 Refresh token placed in the Okta token-request URL (Medium)

`JwtRefreshService.Refresh` posts to `{endpoint}{queryString}{refreshToken}`, where the seed `RefreshTokenQueryString` is `?grant_type=refresh_token&redirect_uri=&scope=offline_access openid&refresh_token=`. The refresh token therefore travels in the URL of a `Content-Type: application/x-www-form-urlencoded` POST instead of the body, contrary to RFC 6749 §6, and is not URL-encoded — exposed to any outbound proxy, HTTP client tracing, or telemetry that records request URIs*. Client authentication uses HTTP Basic with client ID and secret, which is acceptable. The service is registered only when `useJwtRefreshMiddleware` is true. It is verified that no caller exists inside this repository; the identity of the consumer as the external JwtRefresh app is inferred from the redirect-target naming, not independently confirmed*.

### SEC-OIDC-06 Identity.Name mapped to the OIDC name claim (Medium, cross-library)

`ConfigureOpenIdConnectOptions` sets `NameClaimType = "name"` and clears the global `JwtSecurityTokenHandler.DefaultInboundClaimTypeMap`. On external hosts, `SecurityApiHandler.HandleRequirementAsync` (in `SAIF.NetCore.AuthService`) uses `context.User.Identity.Name` (domain-stripped) as the user ID for `SecurityClient.IsAuthorized(userName, role)`. In standard OIDC, `name` is the end-user's full display name, not a unique account identifier; `preferred_username` and `sub` serve that role. If the Okta tenant emits the display name in `name`, AD resource checks query a non-identifier and fail closed; if `name` is user-editable, a user may collide with another account's identifier and inherit its AD resources (privilege escalation)*. The Okta claim configuration is unknown. This finding is recorded here because the mapping originates in this library; enforcement belongs to `SAIF.NetCore.AuthService` and should be cross-referenced from that library's security documentation.

### SEC-OIDC-07 Data-protection keys may be unencrypted at rest (Low–Medium)

The cookie data-protection key ring is stored at `DPKeyDirectory` (seed `C:\OIDC\DP`). The cookie ticket provider calls `ProtectKeysWithCertificate(dpCert)` only if `dpCert != null`; `DataProtectionCertService.GetCertificate` returns `null` without error when `IEnvironmentClient.IsDeveloperMachine()` is true. If a server is ever mis-classified as a developer machine, the shared cookie keys are written in plain XML and could decrypt and forge authentication tickets for every application sharing `DPApplicationName`. On non-developer machines, a missing or expired certificate throws `MissingDataProtectionCertificateException`, so the provider fails closed. The OIDC handler's own data-protection provider (state/correlation, app name `{DPApplicationName}-oidc`) never calls `ProtectKeysWithCertificate` at all — its keys sit unencrypted in the same directory regardless of developer-machine status; a source-code comment attributed to ryadug (dated 2026-07-29) notes that the `-oidc` application-name suffix exists only to isolate this unencrypted key ring from the cookie ring and can be removed once certificate protection is likewise disabled for the cookie ring. Mitigation depends on NTFS ACLs on `DPKeyDirectory`**. The certificate is selected by an exact `Subject` match plus a validity window, with no thumbprint pinning, so any valid certificate in the store with that subject is accepted.

### SEC-OIDC-08 x-idp wildcard query parameters forwarded to the IdP (Low)

`BeforeRedirectToIdentityProviderAsync` copies every incoming request query parameter whose name starts with `x-idp-` into the Okta authorize request, plus the `sessionToken`, `idp`, and `login_hint` authentication properties. This lets an external link inject arbitrary `x-idp-*` parameters into the hosted login widget (the library itself uses `x-idp-err=se`). Whether the widget renders these values without encoding is outside the repository**. The library applies no validation. Okta ignores unknown authorize parameters, so protocol integrity is not affected*.

### SEC-OIDC-09 Raw JWTs stored as identity claims — Low

`JwtClaimMiddleware` runs on every authenticated external request and rewrites the cookie with `id_token` and `access_token` claims that duplicate the token values already held in `AuthenticationProperties`, for legacy-application access. Effects: any code that enumerates or serializes `User.Claims` (diagnostics, logging) exposes bearer tokens*; cookie size roughly doubles; the cookie is re-serialized and reissued (`SignInAsync`) on every request, repeating that response-size/performance cost each time. This reissuance does not itself renew the ticket's sliding-expiration lifetime: `RewriteAuthCookie` forwards the existing `AuthenticationProperties` unchanged, and `UpdateJwtTokenValues` changes token claim values, not `IssuedUtc`/`ExpiresUtc`. The tokens are encrypted in the cookie, so they are not exposed on the wire.

### SEC-OIDC-10 Non-thread-safe singleton cache (Low, latent)

`ClaimTransformationCacheService` is registered as a singleton but backs its state with a plain `Dictionary<string, DateTime>`. Concurrent `Cache`, `Exists`, or `Remove` calls can corrupt the dictionary or throw*. A principal without a `sub` claim passes `null` as the key, which throws `ArgumentNullException`. The dictionary also grows without bound, one entry per subject, with eviction only on lookup. All three are availability (denial-of-service) risks only, and are latent because the transformer is not invoked ([SEC-OIDC-03](#sec-oidc-03-roleclaimtransformer-not-registered-as-iclaimstransformation-%E2%80%94-medium)).

### SEC-OIDC-11 Cookie transport policy implicit (Low)

The cookie is configured `HttpOnly = true`, `SameSite = Lax`, `IsEssential = true`, `Path = "/"`, `SlidingExpiration = true`, and `ExpireTimeSpan = CookieExpireMinutes` (seed 60). `Cookie.SecurePolicy` is not set, so it defaults to `SameAsRequest`. `AppStartup` always applies `UseHsts()`, but applies `UseHttpsRedirection()` only when the machine variable `ASPNETCORE_REDIRECT_SSL` is not `false`. If HTTP is reachable and redirection is disabled, the shared authentication cookie — which contains the refresh token — can be issued without the `Secure` flag (code verified; bindings unverified**).

### SEC-OIDC-12 RewriteAuthCookie wrong-variable bug (Informational)

In `HttpContextExtensions.RewriteAuthCookie`, when `accessToken == null` the code assigns `idToken` instead of `accessToken`, and the subsequent `AddUpdateClaim(OpenIdConnectParameterNames.AccessToken, accessToken)` call throws `ArgumentNullException` via the `Claim` constructor. The method has no callers in the repository; the middleware uses `JwtAuthorizationCookieClaimService` instead. It poses no current security impact, but this public extension should not be adopted as-is.

### Positive controls (verified)

| Control | Setting |
| --- | --- |
| Authorization-code flow | `ResponseType = code` |
| HTTPS metadata | `RequireHttpsMetadata = true` |
| Strict token validation | Signed tokens required; issuer, audience (= client ID), signing key, and lifetime validated; `RequireExpirationTime`; clock skew from `ClockSkewSeconds` (seed 5s); Okta's `StrictSecurityTokenValidator` |
| Client secret handling | `ClientSecret` comes from the secure Environment API setting `BOLAppCredentials`, retrieved with a system hash; it is never a literal |
| Remote-failure handling | Correlation and state failures are logged and redirected to the login page with `x-idp-err=se`; there is no 500 and no stack trace |
| Fail-closed certificate | A missing data-protection certificate on a non-developer server throws at configuration time |
| Authentication failure in middleware | `JwtClaimMiddleware` throws `HttpContextAuthenticateException` instead of continuing with a broken principal |

## Open questions

| ID | Question | Impacts |
| --- | --- | --- |
| Q-S01 | Does the production Environment API store `NetCore_OIDC.RoleClaimIdentitifier` (typo) or only `RoleClaimIdentifier`? | Whether SEC-OIDC-02 resolves to `""` (match all) when activated |
| Q-S02 | Do consumer applications outside this repo register `RoleClaimTransformer` as `IClaimsTransformation`? | If yes, SEC-OIDC-02 is live in those apps (Critical) |
| Q-S03 | How does the JwtRefresh app read `refreshToken` and `origin`? Does it validate `origin` as local, and does it suppress Referer? | SEC-OIDC-01 exposure and open-redirect risk |
| Q-S04 | Do IIS access logs, proxies, or APM tools on external hosts record query strings? | SEC-OIDC-01 blast radius |
| Q-S05 | Which value does the Okta authorization server emit in `name`? Is it user-editable? | SEC-OIDC-06 and SEC-OIDC-02 escalation feasibility |
| Q-S06 | Do the external IIS site bindings and `AllowedHosts` restrict the `Host` header? | SEC-OIDC-04 |
| Q-S07 | What NTFS ACLs apply to `DPKeyDirectory`? Can `IsDeveloperMachine()` ever be true on a server? | SEC-OIDC-07 |
| Q-S08 | Resolved: the Environment API key lookup is case-insensitive — `BaseEnvironmentClient` lower-cases the composed key before comparison (confirmed in [Configuration](configuration.md)), so the `BolAppCredentials` seed vs `BOLAppCredentials` constant casing difference does not affect resolution. The genuinely open item is a spelling (not casing) mismatch, tracked separately under Q-S01 (`RoleClaimIdentitifier` vs `RoleClaimIdentifier`). | Credential resolution — resolved; no further action needed |
| Q-S09 | Is HTTP reachable on any external host with `ASPNETCORE_REDIRECT_SSL=false`? | SEC-OIDC-11 |

## Notes
- This library references no AD resource names; the standard note about `Resource`/`TestResource` suffixes does not apply.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.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/Options/ConfigureCookieAuthenticationOptions.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Options/ConfigureOpenIdConnectOptions.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/JwtClaimMiddleware.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/RefreshJwtMiddleware.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Middleware/LowerCaseUrlMiddleware.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/LoginRedirectService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimTransformer.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/RoleClaimService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/ClaimTransformationCacheService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Services/OidcSettings.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/UrlCaseService.cs`
- `src/SAIF.NetCore.OIDC/src/SAIF.NetCore.OIDC/Exceptions/HttpContextAuthenticateException.cs`
- `src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/IOidcSettings.cs` (consumer dependency; defines the interfaces this library implements)
- `https://raw.githubusercontent.com/dotnet/aspnetcore/v8.0.0/src/Security/Authentication/Cookies/src/CookieAuthenticationHandler.cs` (independent reference for `HandleSignInAsync` preserving an existing `IssuedUtc` and sliding-expiration renewal behavior)
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs` (consumer composition; registers every service/middleware this page describes)
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs` (consumer composition; wires the middleware pipeline order this page describes)
- `src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiHandler.cs` (consumer/cross-library dependency; performs the resource authorization this page references)
