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 populateJwtBearerOptionsfrom environment settings viaConfigureJwtBearerOptions. - Abstract the current-user identity behind
IIdentityService(ServiceContracts) with two concrete implementations —TokenIdentityService(JWT; user identity from themailidclaim) andWinIdentityService(IIS/NTLM; user identity fromIdentity.Name) — selected per request byIdentityServiceAccessororMultiIdentityServiceAccessor. - Call the remote SecurityAPI service through
ISecurityClient/SecurityClientfor 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 asksISecurityClient.IsAuthorizedfor each comma-separated role. - Acquire and cache outbound OAuth client-credentials access tokens via
OktaTokenClient/IAccessTokenClientfor calling downstream APIs.
Component boundaries¶
Extensions.AuthenticationServiceExtensions: the composition surface — eightIServiceCollection/AuthenticationBuilderextension methods, plus the public static mutableAuthenticationSchemeSelectorfield used by both the"dual"policy scheme andMultiIdentityServiceAccessor.ConfigureJwtBearerOptions: anIConfigureNamedOptions<JwtBearerOptions>that reads theoauthenvironment-settings section through the injectedIEnvironmentClient(ServiceContracts).Identitynamespace:BaseIdentityService(abstract, implementsIIdentityService),TokenIdentityService,WinIdentityService,IdentityServiceAccessor, andMultiIdentityServiceAccessor(implementsIIdentityServiceAccessor, ServiceContracts).Clientsnamespace:SecurityClient(implementsISecurityClient, calls the remote SecurityAPI through the injectedIHttpJsonClient, ServiceContracts),SecurityClientException(extendsSAIFException, ServiceContracts), andOktaTokenClient/OktaToken(implementsIAccessTokenClient, calls an OAuth token endpoint through a rawHttpClient).Authorizationnamespace (note: this namespace isSAIF.NetCore.Services.Authorization, distinct from the library'sRootNamespaceSAIF.NetCore.Services.Authentication*):SecurityApiAuthorize(attribute),SecurityApiPolicy(implementsIAuthorizationPolicyProvider),SecurityApiRequirement(implementsIAuthorizationRequirement),SecurityApiHandler(extendsAuthorizationHandler<SecurityApiRequirement>).- This library implements
IIdentityService/IIdentityServiceAccessorand consumesIEnvironmentClient/IHttpJsonClient/IHttpRequestResult/IUserProfile/ExternalUser/SAIFException(all ServiceContracts), but does not referenceSAIF.NetCore.EnvironmentClientorSAIF.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
AddDualAuthenticationis used, the"dual"policy scheme'sForwardDefaultSelector(AuthenticationSchemeSelector) inspects theAuthorizationheader 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 throughSecurityApiPolicy, which builds a policy requiring aSecurityApiRequirement;SecurityApiHandlerthen callsISecurityClient.IsAuthorized(user, role)for each role (OR semantics — any one role succeeding is sufficient). - Application code resolves the current identity through
IIdentityServiceAccessor.IdentityService, which returnsTokenIdentityServiceorWinIdentityServicedepending on the authentication scheme in effect; both ultimately callSecurityClient.IsAuthorizedfor remote authorization checks (withTokenIdentityServiceshort-circuiting on a localUser.IsInRolematch first). SecurityClientissues all remote calls through the injectedIHttpJsonClient(ServiceContracts) against a base URL resolved from theSecurityClient:ServiceUrlenvironment setting;OktaTokenClientissues its own token-endpoint calls through a separately constructedHttpClient, independent ofIHttpJsonClient.- Composition (calling
AddJwtBearer(), registeringConfigureJwtBearerOptions, and selecting whichAdd*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.ServiceExtensionsperforms this composition (RegisterEdgeAPIAuthServices,RegisterCoreAPIAuthServices). SAIF.NetCore.NtlmApiAuthis the one documented implementation-to-implementation exception inArchitecture.md: it takes a direct NuGet dependency on this library to injectISecurityClientinto its ownNtlmApiAuthorizationService.
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.ServiceContractsNuGet package, consistent with the repository-wide layering inArchitecture.md. AuthenticationSchemeSelectoris apublic staticmutable field onAuthenticationServiceExtensions.AddDualAuthentication()captures its delegate value at configuration time into the"dual"policy scheme'sForwardDefaultSelector, whileMultiIdentityServiceAccessorreads the static field fresh on every invocation; reassigning the field after startup changesMultiIdentityServiceAccessor's next resolution immediately, but does not retroactively change theForwardDefaultSelectordelegate already captured by an already-configured"dual"scheme*.OktaTokenClientis registered as a singleton but holds mutable token state and settable credential properties without synchronization, constructs a newHttpClientper token refresh, and blocks synchronously (.Wait()/.Result); it depends onIHttpContextAccessor.HttpContextonly to buildredirect_urion the refresh path — a cached, still-valid token is returned without touchingHttpContext, so callingGetToken()outside a request context only throwsNullReferenceExceptionwhen 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.UserIdthrowsNullReferenceExceptionif no claim type containsmailid;BaseIdentityService.Claims/IsAuthenticateddereferenceIdentitywithout a null check.AddTokenIdentityService()/AddWinIdentityService()each register exactly oneIIdentityServiceinterface mapping (toTokenIdentityServiceorWinIdentityServicerespectively).AddIdentityAccessor()is a different, multi-scheme registration method: it always registers bothTokenIdentityServiceandWinIdentityServiceas concrete self-bindings plusMultiIdentityServiceAccessor, rather than mapping a singleIIdentityServiceinterface.MultiIdentityServiceAccessorresolves 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,GetServicereturnsnullfor the other scheme*.- Composition (which authentication scheme(s) to enable, and registering
IEnvironmentClient/IHttpJsonClientimplementations) is left entirely to the consuming application or toSAIF.NetCore.ServiceExtensions; this library performs no automatic registration of its own dependencies.
Related documentation¶
- Index
- Overview
- Dependencies
- Configuration
- Operations and processes
- Business rules
- Security and authorization
- Service registration
- API reference
- SAIF.NetCore.ServiceContracts overview
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.csprojsrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/ConfigureJwtBearerOptions.cssrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Authorization/SecurityApiAuthorize.cs,SecurityApiHandler.cs,SecurityAPIPolicy.cs,SecurityApiRequirement.cssrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Clients/OktaTokenClient.cs,SecurityClient.cs,SecurityClientException.cssrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Extensions/AuthenticationServiceExtensions.cssrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Identity/BaseIdentityService.cs,IdentityServiceAccessor.cs,MultiIdentityServiceAccessor.cs,TokenIdentityService.cs,WinIdentityService.cssrc/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/SAIF.NetCore.AuthService/Interfaces/IAccessTokenClient.cs,ISecurityClient.cssrc/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/SAIF.NetCore.ServiceContracts/IIdentityService.cs,IEnvironmentClient.cs,IHttpJsonClient.cs,IHttpRequestResult.cs,SAIFException.cs,IUserProfile.cs,UserProfile.cs,ExternalUser.cssrc/SAIF.NetCore.NtlmApiAuth/src/SAIF.NetCore.NtlmApiAuth/SAIF.NetCore.NtlmApiAuth.csprojArchitecture.md