using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Security.Jwt; using ZB.MOM.WW.OtOpcUa.Security.Ldap; namespace ZB.MOM.WW.OtOpcUa.Security; /// /// DI registration for OtOpcUa auth. Single Cookie scheme (the JWT lives inside the /// cookie as its credential payload); no JwtBearer parallel scheme. Matches ScadaBridge /// structurally — see docs/plans/2026-05-29-auth-alignment-design.md. /// public static class ServiceCollectionExtensions { /// /// Wires cookie authentication, DataProtection key persistence to ConfigDb, /// LDAP services, and the LDAP-backed JwtTokenService. Browser flows redirect to /// /login; AJAX/JSON callers receive 401 (handled by the framework's default /// challenge heuristic). /// /// The service collection. /// The application configuration root. public static IServiceCollection AddOtOpcUaAuth(this IServiceCollection services, IConfiguration configuration) { services.AddOptions().Bind(configuration.GetSection(JwtOptions.SectionName)); services.AddOptions().Bind(configuration.GetSection(OtOpcUaCookieOptions.SectionName)); services.AddOptions().Bind(configuration.GetSection(LdapOptions.SectionName)); services.AddSingleton(); // Singleton — LdapAuthService is stateless (creates an LdapConnection per call) and // must be consumable by the Singleton LdapOpcUaUserAuthenticator on driver-role nodes. services.AddSingleton(); services.AddDataProtection() .PersistKeysToDbContext() .SetApplicationName("OtOpcUa"); services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(o => { // Static fields only — Name / ExpireTimeSpan / SecurePolicy / SlidingExpiration // are bound from OtOpcUaCookieOptions in the PostConfigure block below. o.LoginPath = "/login"; o.LogoutPath = "/auth/logout"; o.Cookie.HttpOnly = true; o.Cookie.SameSite = SameSiteMode.Strict; // No OnRedirectToLogin / OnRedirectToAccessDenied overrides — let the framework's // built-in IsAjaxRequest heuristic do its thing (302 for browsers, 401 for AJAX). }); // Externalised cookie config — mirrors ScadaBridge's PostConfigure pattern. Fixes a // pre-existing latent bug where OtOpcUaCookieOptions was bound but ignored. services.AddOptions(CookieAuthenticationDefaults.AuthenticationScheme) .Configure, ILoggerFactory>((cookieOpts, ourOpts, lf) => { var v = ourOpts.Value; cookieOpts.Cookie.Name = v.Name; cookieOpts.ExpireTimeSpan = TimeSpan.FromMinutes(v.ExpiryMinutes); cookieOpts.SlidingExpiration = true; cookieOpts.Cookie.SecurePolicy = v.RequireHttpsCookie ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest; if (!v.RequireHttpsCookie) { lf.CreateLogger("ZB.MOM.WW.OtOpcUa.Security").LogWarning( "Security:Cookie:RequireHttpsCookie is DISABLED — auth cookie SecurePolicy is " + "SameAsRequest. The cookie-embedded JWT will travel in cleartext over plain HTTP. " + "Intended for local dev only — set Security:Cookie:RequireHttpsCookie=true in production."); } }); services.AddAuthorization(o => { o.FallbackPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder( CookieAuthenticationDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build(); // DriverOperator: may issue Reconnect/Restart commands against live driver instances // from the Admin UI DriverStatusPanel. Map LDAP group → role via GroupToRole in // appsettings (e.g. "ot-driver-operator": "DriverOperator"). o.AddPolicy("DriverOperator", policy => policy.RequireRole("DriverOperator", "FleetAdmin")); }); return services; } }