Skip to content

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.

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.

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 and OP-12); 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.

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