SAIF.NetCore.ServiceExtensions security and authorization¶
This library is a security composition root, not an authorization engine. It defines no authorization mechanism of its own. Through its IServiceCollection extensions and abstract Startup base classes, it wires the authentication schemes, the REST SecurityApi authorization policy provider/handler, and the identity accessors — all implemented in SAIF.NetCore.AuthService and SAIF.NetCore.OIDC — into consuming applications. It also defines two cookie-presence "gate" middlewares of its own: one wired (AspNetSessionIdMiddleware) and one not (LegacyBolAuthMiddleware).
Role decisions run through the REST SecurityApi client (ISecurityClient, from SAIF.NetCore.AuthService), not through the WCF SecurityService — no WCF service reference, SecurityServiceClient, securityServiceUrl/WCFEndPointAddress key, or /WCF/Security URL exists under this library's source. This library decides which authentication stack and which authorization components a consuming application gets, based on the chosen Startup base class and the ASPNETCORE_HOST_LOCATION environment variable, and it adds two cookie-presence gates of its own.
flowchart TD
A[Consuming application Startup] --> B{Which base class?}
B -->|AppStartup, External| C["RegisterOidcServices +<br>RegisterCoreAPIAuthServices-shaped SecurityApi wiring"]
B -->|AppStartup, Internal/NotDefined| D[RegisterCoreAPIAuthServices]
B -->|CoreAPIStartup| D
B -->|EdgeAPIStartup| E["RegisterEdgeAPIAuthServices - JWT only,<br>no SecurityApi policy provider"]
C --> F[SecurityApiPolicy / SecurityApiHandler]
D --> F
F --> G["ISecurityClient.IsAuthorized role check -<br>AuthService REST call"]
E -.->|SecurityApiAuthorize on Edge throws at runtime, SEC-07| G
No AD group or resource-name literal exists in this library's code. Every role name reaches the authorization handler from a consumer's new SecurityApiAuthorize(roles) attribute, where roles is a caller-supplied, comma-separated string argument, resolved through the SecurityAPI.<roles> dynamic policy (SecurityAPIPolicy, registered by this library). This library never supplies a role itself.
Resource name
Context
Enforcement point (as wired by this library)
Confidence
(none hard-coded)
Role names are supplied by consumers through new SecurityApiAuthorize(roles), where roles is a caller-supplied string. The comma-separated list is OR-evaluated by SecurityApiHandler.
This library contains no [SaifAuthorize] / [SecurityApiAuthorize] / [Authorize] decoration on any of its own public types. Its enforcement points fall into three kinds: (A) security service registrations made on behalf of consumers, (B) pipeline placement, and (C) the two cookie-gate middlewares.
— (credential plumbing, not authorization). ISystemHashService is bound to the calling assembly, so secure-setting access is keyed on the caller's system hash.
Wired handler semantics (summarized from AuthService; relevant here because this library activates the handler): SecurityApiHandler.HandleRequirementAsync returns without succeeding when User.Identity.Name is blank. It strips a DOMAIN\ prefix, splits the required roles on ,, and calls ISecurityClient.IsAuthorized(user, role) for each one, succeeding on the first true (OR semantics). The policy provider activates only for policy names starting with SecurityAPI.
(1) The session gate runs before the error handler and before authentication. (2) UseStaticFiles runs before UseAuthorization, so static files are served without authorization (standard ASP.NET Core behavior*). (3) ConfigureAuthorization is protected virtual — a derived class can drop UseAuthorization entirely.
With DisableAuthServices = true, UseAuthorization still runs, but no authentication scheme is registered — any [Authorize] endpoint then fails at challenge time (InvalidOperationException: no default challenge scheme*).
No startup calls RequireAuthenticatedUser() (A4); none of the base Startup classes applies a global authenticated-user requirement by default. Each endpoint is protected only if the consumer decorates it with [Authorize]/[SecurityApiAuthorize] or calls RequireAuthenticatedUser() itself — unprotected-by-default is the composed posture.
AspNetSessionIdMiddleware — wired, fail-open gate (SEC-01, High). Checks for presence only (Request.Cookies["ASP.NET_SessionId"] == null); never reads or validates the value (src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/AspNetSessionIdMiddleware.cs L37–39). On a missing cookie, redirects to the LegacySessionProvider endpoint (L41–44), but then unconditionally calls await next(context) (L47) — the redirect is advisory only; the rest of the pipeline (error handler, authentication, routing, authorization, endpoint) still executes, and any endpoint result that sets its own status/body overwrites the 302. Any client-supplied cookie value, including an arbitrary or expired one, satisfies the gate. Additional observations: the app's own AddSession() issues .AspNetCore.Session, not ASP.NET_SessionId, so only an external legacy session provider on a shared host/domain can set the cookie this gate checks for; the redirect host is derived from Request.Host without host filtering (L43), so a forged Host header produces a Location on an attacker-chosen host (low impact alone, but relevant to shared-cache poisoning); and the gate runs before the error handler (src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs L152–158), so a failure in its settings lookup (L42) escapes the SAIF error handler. Mitigating control: ASP.NET Core authentication/authorization still applies independently to endpoints carrying [Authorize]/[SecurityApiAuthorize] — the defect bypasses only the legacy-session requirement, not decorated-endpoint authorization.
LegacyBolAuthMiddleware — not wired, dead code, same flaw (SEC-02, latent). Checks for presence of either SAIFAUTH or SAIF.BOL.Auth (src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Middleware/LegacyBolAuthMiddleware.cs L36–39), with the identical fail-open defect (unconditional await next(context) at L47 after Redirect at L44). It is registered in DI when AppStartup.EnableLegacyBolAuthMiddleware = true (src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs L55, L113–116), but no UseMiddleware<LegacyBolAuthMiddleware>() call exists anywhere in the repository (repo-wide grep against src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/Startup/AppStartup.cs L144–189) — the flag registers a service that is never invoked, so setting it gives no protection (a misleading-configuration risk). The class is public, so an external consumer could wire it manually**.
Exception detail disclosure to clients. Every ControllerBaseExtensions.Throw*(Exception ex) overload (see Controller throw helpers) passes ex.ToString() (type, message, stack trace, inner exceptions) as the exception message; DynamicErrorHandler/ErrorHandler.LogAndReturnException writes it to the response body for handled exceptions. The response also carries a Server-Name header (machine name).
Credentials logged at Debug level.Debug request handler invocation runs on every request in every startup and logs WebRequestDetails, which captures all request headers (including Authorization and Cookie, which can carry SAIFAUTH) and the full request body with no redaction at the point this library emits the log event. Mitigating control: the LoggingApi sink (SAIF.NetCore.Logging's LoggingApiSink) is reported to apply middleware that identifies and masks credentials and other secure data elements before the log entry is persisted to disk, Splunk, or Dynatrace; this masking is not implemented in, or traceable to source in, this repository.** Exposure at the point of emission (in-process, pre-sink) depends on the effective production log level.
Severity
Low (mitigated at the sink by LoggingApi masking, per the above); Medium at the point of emission if that mitigation is bypassed or the sink is misconfigured
Confidence
verified (code emits unredacted data at the point of logging); unknown (sink-side masking could not be confirmed in source)**
SEC-05 Duplicate security registrations in the External branch¶
Field
Value
Finding
Duplicate security registrations in the External branch.SecurityApiHandler is added as IAuthorizationHandler twice in App startup configure services's External branch; ASP.NET Core runs every registered handler, so each [SecurityApiAuthorize] check calls the SecurityApi twice per role*. ISecurityClient is registered Transient then Singleton, so lifetime depends on resolution style.
Severity
Low (performance/clarity)
Confidence
verified (registrations); inferred (double call)*
SEC-06 Host-location fallback picks the internal auth stack¶
Field
Value
Finding
Host-location fallback picks the internal auth stack. If ASPNETCORE_HOST_LOCATION is missing or unrecognized, HostLocation = NotDefined falls into the internal (dual IIS/JWT) branch instead of OIDC. On an externally facing host, a missing env var silently switches auth stacks, and no OIDC middleware runs.
Severity
Medium (misconfiguration)
Confidence
verified
SEC-07 Edge APIs have no SecurityApi policy provider¶
Field
Value
Finding
Edge APIs have no SecurityApi policy provider.Register Edge API auth services does not call AddSecurityApiAuthorizeAttribute. A consumer applying [SecurityApiAuthorize("X")] on an Edge API resolves the SecurityAPI.X policy through the default policy provider, which is not registered for it — authorization throws at run time instead of enforcing the role (fails closed, but as an error rather than a 403)*.
Severity
Low
Confidence
verified (absence); inferred (runtime behavior)*
SEC-08 DisableAuthServices removes all authentication¶
Field
Value
Finding
DisableAuthServices removes all authentication for CoreAPIStartup derivatives with one protected flag; UseAuthorization still runs with no scheme registered. Intended as an opt-out, but a single switch for the whole auth stack.
Severity
Low (by design)
Confidence
verified
SEC-09 HTTPS redirection can be turned off by a machine env var¶
Field
Value
Finding
HTTPS redirection can be turned off by a machine env var.ASPNETCORE_REDIRECT_SSL=false disables UseHttpsRedirection for AppStartup apps (default true, fail-secure); HSTS stays on. CoreAPIStartup/EdgeAPIStartup always redirect.
Claim parsing in JwtControllerBase (see JWT controller claim helpers): GetUsernameFromClaim takes the first claim whose type containsnameidentifier, splits on @, upper-cases. Any exception yields "Unknown"; a missing claim yields null. Callers using this value for authorization or auditing get a non-validated, substring-matched identity.
Secure credential bootstrap.Register DB connection client splits the connectionservicecreds/dbconnectionclient secure setting on \| without validating the format; a malformed value throws IndexOutOfRangeException at first resolution. No credential literal exists in source.
Severity
Low (robustness)
Confidence
verified
Data-level, segregation-of-duties, and machine authorization¶
None detected. No ownership validation (CheckValidContext, IsValidPolicyForCurrentUser, ValidateOwnership, or equivalent), no composite identity+resource check, no SOD class/method/privilege-combination query, and no machine-account ($-suffix) SAMAccountName handling exists in this library. This library composes framework-level authorization only.
This page covers a REST SecurityApi composition root: there is no WCF SecurityService and no AD-group catalog owned by this library (see AD resource catalog above).
Severity labels (High/Medium/Low/latent) reflect this page's own risk assessment of the composed security posture, not a value read from any source artifact.
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.