Skip to content

SAIF.NetCore.AuthService architecture

SAIF.NetCore.AuthService is an implementation-layer library in the "Auth & Identity" group that configures token- and Windows-based authentication, abstracts the current-user identity, calls a remote SecurityAPI service for role authorization and user/group lookups, and provides a declarative [SecurityApiAuthorize] authorization policy; it depends only on the foundation SAIF.NetCore.ServiceContracts package.

Responsibilities

  • Wire ASP.NET Core authentication schemes (AddJwtAuthentication, AddIISAuthentication, AddDualAuthentication) and populate JwtBearerOptions from environment settings via ConfigureJwtBearerOptions.
  • Abstract the current-user identity behind IIdentityService (ServiceContracts) with two concrete implementations — TokenIdentityService (JWT; user identity from the mailid claim) and WinIdentityService (IIS/NTLM; user identity from Identity.Name) — selected per request by IdentityServiceAccessor or MultiIdentityServiceAccessor.
  • Call the remote SecurityAPI service through ISecurityClient/SecurityClient for role authorization, user-profile retrieval, AD group membership, application authorization groups, and external-user data.
  • Provide declarative role authorization: [SecurityApiAuthorize("roles")] → dynamic policy "SecurityAPI.{roles}" → SecurityApiPolicy → SecurityApiRequirement → SecurityApiHandler, which asks ISecurityClient.IsAuthorized for each comma-separated role.
  • Acquire and cache outbound OAuth client-credentials access tokens via OktaTokenClient/IAccessTokenClient for calling downstream APIs.

Component boundaries

  • Extensions.AuthenticationServiceExtensions: the composition surface — eight IServiceCollection/AuthenticationBuilder extension methods, plus the public static mutable AuthenticationSchemeSelector field used by both the "dual" policy scheme and MultiIdentityServiceAccessor.
  • ConfigureJwtBearerOptions: an IConfigureNamedOptions<JwtBearerOptions> that reads the oauth environment-settings section through the injected IEnvironmentClient (ServiceContracts).
  • Identity namespace: BaseIdentityService (abstract, implements IIdentityService), TokenIdentityService, WinIdentityService, IdentityServiceAccessor, and MultiIdentityServiceAccessor (implements IIdentityServiceAccessor, ServiceContracts).
  • Clients namespace: SecurityClient (implements ISecurityClient, calls the remote SecurityAPI through the injected IHttpJsonClient, ServiceContracts), SecurityClientException (extends SAIFException, ServiceContracts), and OktaTokenClient/OktaToken (implements IAccessTokenClient, calls an OAuth token endpoint through a raw HttpClient).
  • Authorization namespace (note: this namespace is SAIF.NetCore.Services.Authorization, distinct from the library's RootNamespace SAIF.NetCore.Services.Authentication*): SecurityApiAuthorize (attribute), SecurityApiPolicy (implements IAuthorizationPolicyProvider), SecurityApiRequirement (implements IAuthorizationRequirement), SecurityApiHandler (extends AuthorizationHandler<SecurityApiRequirement>).
  • This library implements IIdentityService/IIdentityServiceAccessor and consumes IEnvironmentClient/IHttpJsonClient/IHttpRequestResult/IUserProfile/ExternalUser/SAIFException (all ServiceContracts), but does not reference SAIF.NetCore.EnvironmentClient or SAIF.NetCore.HttpClients — the consuming host must register concrete implementations of those interfaces.

Interaction model

  • An inbound request passes through the ASP.NET Core authentication middleware; when AddDualAuthentication is used, the "dual" policy scheme's ForwardDefaultSelector (AuthenticationSchemeSelector) inspects the Authorization header and forwards to the JWT scheme if it starts with "Bearer ", otherwise to the IIS/Windows scheme.
  • Authorization middleware evaluates [SecurityApiAuthorize("roles")] by resolving the dynamic "SecurityAPI.{roles}" policy through SecurityApiPolicy, which builds a policy requiring a SecurityApiRequirement; SecurityApiHandler then calls ISecurityClient.IsAuthorized(user, role) for each role (OR semantics — any one role succeeding is sufficient).
  • Application code resolves the current identity through IIdentityServiceAccessor.IdentityService, which returns TokenIdentityService or WinIdentityService depending on the authentication scheme in effect; both ultimately call SecurityClient.IsAuthorized for remote authorization checks (with TokenIdentityService short-circuiting on a local User.IsInRole match first).
  • SecurityClient issues all remote calls through the injected IHttpJsonClient (ServiceContracts) against a base URL resolved from the SecurityClient:ServiceUrl environment setting; OktaTokenClient issues its own token-endpoint calls through a separately constructed HttpClient, independent of IHttpJsonClient.
  • Composition (calling AddJwtBearer(), registering ConfigureJwtBearerOptions, and selecting which Add* extension methods to call) is the consuming application's responsibility; this library's extension methods are building blocks, not an opinionated default pipeline. In the standard stack, SAIF.NetCore.ServiceExtensions performs this composition (RegisterEdgeAPIAuthServices, RegisterCoreAPIAuthServices).
  • SAIF.NetCore.NtlmApiAuth is the one documented implementation-to-implementation exception in Architecture.md: it takes a direct NuGet dependency on this library to inject ISecurityClient into its own NtlmApiAuthorizationService.

Architecture diagram

The following diagram shows how SAIF.NetCore.AuthService sits between the ASP.NET Core authentication/authorization pipeline, the ServiceContracts interfaces it depends on, and the remote SecurityAPI/OAuth endpoints it calls.

flowchart TD
    REQ["Inbound request"] -->|auth middleware| DUAL["dual policy scheme<br>(AuthenticationSchemeSelector)"]
    DUAL -->|"JWT scheme marker"| JWT["JwtBearer scheme<br>(ConfigureJwtBearerOptions)"]
    DUAL -->|otherwise| IIS["IIS / Windows scheme"]
    JWT --> IDSVC["IIdentityServiceAccessor<br>→ TokenIdentityService"]
    IIS --> IDSVC2["IIdentityServiceAccessor<br>→ WinIdentityService"]
    ATTR["[SecurityApiAuthorize]"] --> POLICY["SecurityApiPolicy<br>(IAuthorizationPolicyProvider)"]
    POLICY --> HANDLER["SecurityApiHandler<br>(AuthorizationHandler)"]
    HANDLER -->|IsAuthorized per role| SECCLIENT["SecurityClient<br>(ISecurityClient)"]
    IDSVC -->|IsAuthorized fallback| SECCLIENT
    IDSVC2 --> SECCLIENT
    SECCLIENT -->|IHttpJsonClient.GetAsync| API["SecurityAPI<br>(external REST service)"]
    ENVC["IEnvironmentClient<br>(ServiceContracts contract)"] --> JWT
    ENVC --> SECCLIENT
    OKTA["OktaTokenClient<br>(IAccessTokenClient)"] -->|raw HttpClient POST| OAUTH["OAuth token endpoint<br>(external)"]
    NTLM["SAIF.NetCore.NtlmApiAuth<br>(documented exception)"] -.->|NuGet ref| SECCLIENT

Sequence diagram

The following diagram shows the primary request/response sequence for a [SecurityApiAuthorize("role")]-protected endpoint.

sequenceDiagram
    participant Client
    participant AuthZ as ASP.NET Core authorization
    participant Policy as SecurityApiPolicy
    participant Handler as SecurityApiHandler
    participant SecClient as SecurityClient
    participant API as SecurityAPI (external)
    Client->>AuthZ: Request SecurityApiAuthorize(role)
    AuthZ->>Policy: GetPolicyAsync("SecurityAPI.role")
    Policy-->>AuthZ: AuthorizationPolicy(SecurityApiRequirement)
    AuthZ->>Handler: HandleRequirementAsync(requirement)
    Handler->>SecClient: IsAuthorized(userId, "role")
    SecClient->>API: GET /v1/Users/IsAuthorized/{userId}/{suffixRole}
    API-->>SecClient: bool result
    SecClient-->>Handler: bool result
    Handler-->>AuthZ: context.Succeed(requirement) or no-op
    AuthZ-->>Client: 200 OK or 403 Forbidden

Design constraints

  • No project references between libraries in this repository; the only cross-library dependency is the SAIF.NetCore.ServiceContracts NuGet package, consistent with the repository-wide layering in Architecture.md.
  • AuthenticationSchemeSelector is a public static mutable field on AuthenticationServiceExtensions. AddDualAuthentication() captures its delegate value at configuration time into the "dual" policy scheme's ForwardDefaultSelector, while MultiIdentityServiceAccessor reads the static field fresh on every invocation; reassigning the field after startup changes MultiIdentityServiceAccessor's next resolution immediately, but does not retroactively change the ForwardDefaultSelector delegate already captured by an already-configured "dual" scheme*.
  • OktaTokenClient is registered as a singleton but holds mutable token state and settable credential properties without synchronization, constructs a new HttpClient per token refresh, and blocks synchronously (.Wait()/.Result); it depends on IHttpContextAccessor.HttpContext only to build redirect_uri on the refresh path — a cached, still-valid token is returned without touching HttpContext, so calling GetToken() outside a request context only throws NullReferenceException when a refresh is actually needed*. The refreshed token value is also cached and returned without any additional post-refresh validity check.
  • BaseIdentityService.GetUser()/GetUser<T>() use sync-over-async (.Result); TokenIdentityService.UserId throws NullReferenceException if no claim type contains mailid; BaseIdentityService.Claims/IsAuthenticated dereference Identity without a null check.
  • AddTokenIdentityService()/AddWinIdentityService() each register exactly one IIdentityService interface mapping (to TokenIdentityService or WinIdentityService respectively). AddIdentityAccessor() is a different, multi-scheme registration method: it always registers both TokenIdentityService and WinIdentityService as concrete self-bindings plus MultiIdentityServiceAccessor, rather than mapping a single IIdentityService interface. MultiIdentityServiceAccessor resolves one of those two concrete types from the service provider by scheme; if a consumer mixes registration methods such that only one of the two concrete types is registered, GetService returns null for the other scheme*.
  • Composition (which authentication scheme(s) to enable, and registering IEnvironmentClient/IHttpJsonClient implementations) is left entirely to the consuming application or to SAIF.NetCore.ServiceExtensions; this library performs no automatic registration of its own dependencies.

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/SAIF.NetCore.AuthService.csproj
  • 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, SecurityApiHandler.cs, SecurityAPIPolicy.cs, SecurityApiRequirement.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/OktaTokenClient.cs, SecurityClient.cs, SecurityClientException.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Extensions/AuthenticationServiceExtensions.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/BaseIdentityService.cs, IdentityServiceAccessor.cs, MultiIdentityServiceAccessor.cs, TokenIdentityService.cs, WinIdentityService.cs
  • src/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Interfaces/IAccessTokenClient.cs, ISecurityClient.cs
  • src/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/IIdentityService.cs, IEnvironmentClient.cs, IHttpJsonClient.cs, IHttpRequestResult.cs, SAIFException.cs, IUserProfile.cs, UserProfile.cs, ExternalUser.cs
  • src/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/SAIF.NetCore.NtlmApiAuth.csproj
  • Architecture.md