---
title: SAIF.NetCore.OIDC architecture
summary: Architectural responsibilities, boundaries, and interaction patterns.
status: draft
owner: SAIF Framework Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.OIDC
reference_type: package
---

# SAIF.NetCore.OIDC architecture

SAIF.NetCore.OIDC is an implementation-layer library (per `Architecture.md`) that supplies turnkey Okta OpenID Connect and cookie-authentication building blocks for externally-hosted SAIF ASP.NET Core applications — option configurators, authentication middleware, role-claim transformation, data-protection certificate lookup, login redirection, and an Okta refresh-token client — and it depends only on the foundation layer (`SAIF.NetCore.ServiceContracts`), as implementation libraries are expected to do.

## Responsibilities
- Configure the ASP.NET Core cookie and OpenID Connect authentication handlers against Okta (`ConfigureCookieAuthenticationOptions`, `ConfigureOpenIdConnectOptions`).
- Enforce auth-cookie rewriting and JWT claim propagation through `IMiddleware` components (`JwtClaimMiddleware`, `RefreshJwtMiddleware`, `LowerCaseUrlMiddleware`).
- Refresh expired access/ID tokens against the Okta token endpoint (`JwtRefreshService`).
- Transform and cache role claims on the authenticated principal (`RoleClaimTransformer`, `RoleClaimService`, `ClaimTransformationCacheService`).
- Resolve data-protection certificates from the local certificate store for cookie key-ring protection (`DataProtectionCertService`).
- Redirect unauthenticated requests to the login flow, preserving the originating URL (`LoginRedirectService`).
- Resolve all `NetCore_OIDC` environment settings through `IEnvironmentClient` (`OidcSettings`).

## Component boundaries
- `Options/` configurators: own the one-time construction of `CookieAuthenticationOptions` and `OpenIdConnectOptions`, including the Okta authority, token validation parameters, and the `StrictSecurityTokenValidator`/`OktaHttpMessageHandler` wiring from `Okta.AspNetCore`.
- `Middleware/`: own per-request concerns — rewriting the auth cookie after claim updates, redirecting to an external token-refresh application when the access token has already expired (`RefreshJwtMiddleware` only detects and redirects; it does not call `IJwtRefreshService` and does not refresh the token in-process), and normalizing request URL casing.
- `Services/`: own settings resolution (`OidcSettings`), the Okta token refresh call (`JwtRefreshService`), role-claim transformation and its cache, data-protection certificate lookup, and login redirection.
- `Contracts/`/`Exceptions/`: own the `RefreshTokenResult` DTO and the library's three custom exception types.
- All service interfaces this library implements (`IOidcSettings`, `IJwtRefreshService`, `IRoleClaimService`, `IClaimTransformCacheService`, `ILoginRedirectService`, `IJwtAuthorizationCookieClaimService`, `IUrlCaseService`, `IDataProtectionCertService`) are defined in `SAIF.NetCore.ServiceContracts`, not in this library.
- This library ships **no** DI registration extension method; composing and registering these types into `IServiceCollection`/`IApplicationBuilder` is owned entirely by the consumer, `SAIF.NetCore.ServiceExtensions` (`RegisterOidcServices`, `AddWebAuthentication`)*.

## Interaction model
- In the one `ServiceExtensions` composition path inspected for this documentation, consumers do not reference this library's types directly for registration; `SAIF.NetCore.ServiceExtensions` composes and registers every type in this library as part of `RegisterOidcServices`/`AddWebAuthentication`*. This library itself ships no DI registration guard, so a consumer could register its public OIDC types directly instead; whether any does is unknown**.
- At request time, the registered ASP.NET Core authentication pipeline invokes `ConfigureOpenIdConnectOptions`/`ConfigureCookieAuthenticationOptions` to challenge/validate against Okta, and the registered middleware (`JwtClaimMiddleware`, `RefreshJwtMiddleware`, `LowerCaseUrlMiddleware`) runs per request to rewrite cookies, redirect requests with an already-expired access token to an external token-refresh application (ending that request with a 301; it does not call `IJwtRefreshService`), and normalize casing.
- `OidcSettings` resolves every `NetCore_OIDC` setting lazily from `IEnvironmentClient` on each property access; it is not cached locally beyond what `IEnvironmentClient`'s own section cache provides.
- `JwtRefreshService.Refresh` is a separately callable service that exchanges a refresh token for a new access/ID token pair by calling out to `IHttpJsonClient` (implemented by `SAIF.NetCore.HttpClients`) to POST to the Okta token endpoint; no in-repository caller for this service was found, so which process invokes it (for example, a separate JwtRefresh application) is unknown**. `OidcSettings.AppCredentials` separately calls `ISystemHashService` (implemented by `SAIF.NetCore.SystemHash`) to compute a hash used as a secure-setting key.
- All five runtime-required services (`IEnvironmentClient`, `ISystemHashService`, `ISaifLogService`, `IHttpJsonClient`, `IHttpContextAccessor`) are resolved from the DI container; none are referenced as compile-time package dependencies of this library — they are satisfied through `SAIF.NetCore.ServiceContracts` interfaces and bound to concrete implementations only by the consuming host*.

## Architecture diagram
The following diagram shows how this library's components depend on `SAIF.NetCore.ServiceContracts`, how `SAIF.NetCore.ServiceExtensions` composes and registers them, and how the runtime services they require are supplied by sibling implementation libraries at the consumer's composition root.

```mermaid
flowchart TD
    SC["SAIF.NetCore.ServiceContracts<br>(IOidcSettings, IJwtRefreshService, etc.)"]
    OIDC_OPT["OIDC Options<br>ConfigureCookieAuthenticationOptions<br>ConfigureOpenIdConnectOptions"]
    OIDC_MW["OIDC Middleware<br>JwtClaimMiddleware, RefreshJwtMiddleware<br>LowerCaseUrlMiddleware"]
    OIDC_SVC["OIDC Services<br>OidcSettings, JwtRefreshService<br>RoleClaim*, LoginRedirectService<br>DataProtectionCertService"]
    SE["SAIF.NetCore.ServiceExtensions<br>RegisterOidcServices / AddWebAuthentication"]
    ENV["SAIF.NetCore.EnvironmentClient<br>(IEnvironmentClient)"]
    HASH["SAIF.NetCore.SystemHash<br>(ISystemHashService)"]
    HTTP["SAIF.NetCore.HttpClients<br>(IHttpJsonClient)"]
    LOG["SAIF.NetCore.Logging<br>(ISaifLogService)"]
    OKTA["Okta<br>(external OIDC provider)"]
    APP["Consuming application<br>(externally hosted BOL)"]

    OIDC_OPT -.->|NuGet ref| SC
    OIDC_MW -.->|NuGet ref| SC
    OIDC_SVC -.->|NuGet ref| SC
    SE -.->|NuGet ref| OIDC_OPT
    SE -.->|NuGet ref| OIDC_MW
    SE -.->|NuGet ref| OIDC_SVC
    APP -.->|NuGet ref| SE
    OIDC_SVC -->|DI-resolved at host composition root| ENV
    OIDC_SVC -->|DI-resolved at host composition root| HASH
    OIDC_SVC -->|DI-resolved at host composition root| HTTP
    OIDC_MW -->|DI-resolved at host composition root| LOG
    OIDC_OPT -->|authorize/token/userinfo| OKTA
    OIDC_SVC -->|POST refresh token| OKTA
```

## Sequence diagram
The following diagram shows the primary request sequence for an authenticated request whose access token has already expired: `RefreshJwtMiddleware` decodes the token (without signature validation), detects `ValidTo < UtcNow`, and ends the request with a 301 redirect to the external token-refresh application — it never calls `IJwtRefreshService` or `JwtRefreshService.Refresh`.

```mermaid
sequenceDiagram
    participant Client
    participant RefreshJwtMiddleware
    participant JwtRefreshApp as "External JwtRefresh application"

    Client->>RefreshJwtMiddleware: Request with expired access token
    RefreshJwtMiddleware->>RefreshJwtMiddleware: Decode token (no signature check), detect ValidTo < UtcNow
    RefreshJwtMiddleware-->>Client: 301 redirect to RefreshTokenRedirectUrl (refreshToken + origin in query)
    Note over Client,JwtRefreshApp: Request ends here. Any downstream call this<br>external application makes back into this library's<br>JwtRefreshService is unknown/inferred** — no in-repo caller exists.
```

`JwtRefreshService.Refresh` is documented separately as a standalone, callable service (see [JwtRefreshService](api/saif.netcore.oidc.services/jwtrefreshservice.md) and [OP-12](operations/op-12-refresh-tokens.md)); it exchanges a refresh token for a new `JwtSet` by posting to Okta's token endpoint, but this library has no in-repository caller for it, so the sequence by which any consumer (for example, the JwtRefresh application referenced above) invokes it is not verified here**.

## Design constraints
- The library takes **no NuGet dependency on any other SAIF implementation library** — only on `SAIF.NetCore.ServiceContracts` — consistent with the repo-wide rule that implementation libraries depend solely on the foundation layer*.
- Okta is a hard-coded external identity provider dependency (`Okta.AspNetCore`); there is no abstraction allowing a different OIDC provider without replacing the options configurators.
- The role-transform cache (`ClaimTransformationCacheService`) is an in-process singleton backed by a plain, non-thread-safe `Dictionary`, so it does not survive process restarts and is not safe for unsynchronized concurrent access.
- `RefreshJwtMiddleware`/`JwtRefreshService` send the OAuth refresh grant in the query string, not the request body, and the refresh token value is not URL-encoded.
- The data-protection key ring depends on local file-system and Windows certificate-store access, coupling the library's cookie-protection behavior to the host machine's certificate configuration.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.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`
- Architecture.md
