using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.Roles; using ZB.MOM.WW.Auth.AspNetCore; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; namespace ZB.MOM.WW.ScadaBridge.Security; public static class ServiceCollectionExtensions { /// /// The configuration section bound to the shared LDAP LdapOptions. Nested /// under the existing ScadaBridge:Security section as a Ldap sub-section /// (Task 1.4 config rename) so the non-LDAP fields stay /// where they are while the LDAP connection settings bind to the shared library. /// The Host composition root references this constant to register the shared LDAP /// auth (AddZbLdapAuth) before calling . /// public const string LdapSectionPath = "ScadaBridge:Security:Ldap"; /// /// Registers the JWT token service, role mapper, cookie authentication, and /// authorization policies. This is a component library and therefore takes no /// IConfiguration (Options pattern only). /// /// /// LDAP authentication (shared ZB.MOM.WW.Auth.Ldap via AddZbLdapAuth) is /// registered by the Host composition root — which calls AddZbLdapAuth with the /// nested before this method — because that /// registration is config-coupled (it binds LdapOptions from /// IConfiguration) and component libraries must not accept IConfiguration. /// /// The service collection to register into. public static IServiceCollection AddSecurity(this IServiceCollection services) { // Task 1.2 cutover: ScadaBridge's bespoke LdapAuthService was replaced by the // shared ZB.MOM.WW.Auth.Ldap implementation (ScadaBridge was the donor for its // hardened bind-then-search / escaping / fail-closed semantics, so this was a // behaviour-equivalent re-point). That registration — AddZbLdapAuth, which binds // LdapOptions from the nested Ldap sub-section, registers IValidateOptions // with ValidateOnStart (so a misconfigured directory fails fast at boot — // superseding the old SecurityOptionsValidator LDAP checks), and registers // ILdapAuthService as a stateless singleton — now lives at the Host composition // root (which calls AddZbLdapAuth(configuration, LdapSectionPath) immediately // before AddSecurity()), because it is config-coupled and this is a component // library. services.AddScoped(); services.AddScoped(); // Audit Actor wiring (Phase 3): the user-facing inbound API audit path // sources AuditEvent.Actor from the authenticated principal via this // seam. HttpAuditActorAccessor reads IHttpContextAccessor.HttpContext?.User // (canonical username claim, Identity.Name fallback) and returns null when // there is no authenticated interactive user — so the caller keeps its // existing actor/fallback (API-key name, "system"). Registered as a // singleton (it is stateless and only dereferences the ambient request); // AddHttpContextAccessor is idempotent (TryAdd-based) so calling it here // is safe even though the Host's AddCentralUI also registers it. services.AddHttpContextAccessor(); services.AddSingleton(); // Auth-adoption Task 1.1: register the shared IGroupRoleMapper // seam additively, wrapping RoleMapper to reuse its DB-backed mapping + // site-scope union semantics. Scoped to match RoleMapper's lifetime (it // depends on the Scoped ISecurityRepository). The existing RoleMapper // registration and its call sites are left untouched — login is rewired // to consume this seam in a later task. services.AddScoped, ScadaBridgeGroupRoleMapper>(); // Note: the old SecurityOptionsValidator (which fail-fast-validated LdapServer + // LdapSearchBase) is gone — those keys moved into the shared LdapOptions, whose // LdapOptionsValidator (registered with ValidateOnStart by AddZbLdapAuth above) // now enforces Server + SearchBase + ServiceAccountDn + transport at startup. The // JWT signing key continues to fail-fast at JwtTokenService construction. // Register ASP.NET Core authentication with cookie scheme. The non- // SecurityOptions-coupled settings (paths, cookie name) are set here; the // hardened cookie defaults that depend on SecurityOptions (idle timeout, // HTTPS policy) are applied via the SecurityOptions-bound PostConfigure // below through ZbCookieDefaults.Apply. services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options => { options.LoginPath = "/login"; options.LogoutPath = "/auth/logout"; // The cookie NAME is app-owned and not set by ZbCookieDefaults.Apply // (so co-hosted ZB apps do not clobber each other's session). Keep // ScadaBridge's existing name so live sessions survive this change. options.Cookie.Name = "ZB.MOM.WW.ScadaBridge.Auth"; // HttpOnly / SameSite / SecurePolicy / SlidingExpiration / // ExpireTimeSpan are all set by ZbCookieDefaults.Apply in the // SecurityOptions-bound PostConfigure below. }); // CentralUI-005: configure the cookie session as a sliding window so the // code matches the documented policy ("sliding refresh, 30-minute idle // timeout"). ASP.NET cookie auth exposes a single ExpireTimeSpan plus a // SlidingExpiration flag — it cannot natively model a 15-minute sliding // token AND a separate 30-minute absolute idle cap. The faithful // interpretation: the cookie window IS the idle timeout // (SecurityOptions.IdleTimeoutMinutes, default 30) and SlidingExpiration // renews it on activity (the middleware re-issues the cookie once past // the halfway mark of the window). An active user is therefore kept // signed in; an idle user is signed out after the idle timeout. The // 15-minute JwtExpiryMinutes governs the lifetime of the embedded JWT // itself (see JwtTokenService) — a separate layer from the cookie // session window. Bound here via PostConfigure so SecurityOptions // (configured by the Host after AddSecurity) is honoured. // // Task 1.5: the cookie hardening (HttpOnly=true, SameSite=Strict, // SecurePolicy, SlidingExpiration=true, ExpireTimeSpan=idle) now comes from // the shared ZbCookieDefaults.Apply, with requireHttps + idleTimeout driven // by SecurityOptions so behaviour (30-min sliding idle window, HTTPS-only // unless explicitly opted out) is preserved. services.AddOptions(CookieAuthenticationDefaults.AuthenticationScheme) .Configure, ILoggerFactory>((cookieOptions, securityOptions, loggerFactory) => { ZbCookieDefaults.Apply( cookieOptions, requireHttps: securityOptions.Value.RequireHttpsCookie, idleTimeout: TimeSpan.FromMinutes(securityOptions.Value.IdleTimeoutMinutes)); // Security-021: when the operator opts out of HTTPS-only cookies, // log a Warning so an HTTP-only deployment is at least audible in // the startup log. The cookie carries the embedded JWT bearer // credential — over plain HTTP that travels in cleartext on every // request. The default is true; this branch fires only on an // explicit opt-out (typically the dev Docker cluster). Apply sets // SecurePolicy=SameAsRequest in that case. if (!securityOptions.Value.RequireHttpsCookie) { loggerFactory.CreateLogger("ZB.MOM.WW.ScadaBridge.Security").LogWarning( "SecurityOptions:RequireHttpsCookie is DISABLED — auth cookie SecurePolicy is SameAsRequest. The cookie-embedded JWT will be transmitted in cleartext over plain HTTP. This setting is intended for local dev only — set SecurityOptions:RequireHttpsCookie=true in production."); } }); services.AddScadaBridgeAuthorization(); return services; } /// /// Registers security-related Akka actors (placeholder for future actor registrations). /// /// The service collection to register into. public static IServiceCollection AddSecurityActors(this IServiceCollection services) { // Phase 0: placeholder for Akka actor registration return services; } }