110 lines
5.7 KiB
C#
110 lines
5.7 KiB
C#
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.DataProtection;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using ZB.MOM.WW.Auth.Abstractions.Roles;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// 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 <c>docs/plans/2026-05-29-auth-alignment-design.md</c>.
|
|
/// </summary>
|
|
public static class ServiceCollectionExtensions
|
|
{
|
|
/// <summary>
|
|
/// Wires cookie authentication, DataProtection key persistence to ConfigDb,
|
|
/// LDAP services, and the LDAP-backed JwtTokenService. Browser flows redirect to
|
|
/// <c>/login</c>; AJAX/JSON callers receive 401 (handled by the framework's default
|
|
/// challenge heuristic).
|
|
/// </summary>
|
|
/// <param name="services">The service collection.</param>
|
|
/// <param name="configuration">The application configuration root.</param>
|
|
public static IServiceCollection AddOtOpcUaAuth(this IServiceCollection services, IConfiguration configuration)
|
|
{
|
|
services.AddOptions<JwtOptions>().Bind(configuration.GetSection(JwtOptions.SectionName));
|
|
services.AddOptions<OtOpcUaCookieOptions>().Bind(configuration.GetSection(OtOpcUaCookieOptions.SectionName));
|
|
services.AddOptions<LdapOptions>().Bind(configuration.GetSection(LdapOptions.SectionName));
|
|
|
|
services.AddSingleton<JwtTokenService>();
|
|
// Singleton — LdapAuthService is stateless (creates an LdapConnection per call) and
|
|
// must be consumable by the Singleton LdapOpcUaUserAuthenticator on driver-role nodes.
|
|
// TryAdd so a fused admin+driver node (which also registers it in Program.cs for the
|
|
// driver path) ends up with exactly one descriptor regardless of registration order.
|
|
services.TryAddSingleton<ILdapAuthService, LdapAuthService>();
|
|
|
|
// Shared ZB.MOM.WW.Auth group→role mapper seam (Task 1.1, additive). Wraps the existing
|
|
// RoleMapper.Map + RoleMapper.Merge logic; the login flow is rewired to consume it in a
|
|
// later task. Scoped to match ILdapGroupRoleMappingService (DbContext-backed, registered
|
|
// Scoped) — a singleton here would capture the scoped DB service.
|
|
services.TryAddScoped<IGroupRoleMapper<string>, OtOpcUaGroupRoleMapper>();
|
|
|
|
services.AddDataProtection()
|
|
.PersistKeysToDbContext<OtOpcUaConfigDbContext>()
|
|
.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<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
.Configure<IOptions<OtOpcUaCookieOptions>, 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 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"));
|
|
|
|
// FleetAdmin: full administrative access; gates fleet-wide pages such as RoleGrants.
|
|
o.AddPolicy("FleetAdmin", policy => policy.RequireRole("FleetAdmin"));
|
|
});
|
|
|
|
return services;
|
|
}
|
|
}
|