feat(security): wire DisableLogin flag — auto-login scheme + startup warning

This commit is contained in:
Joseph Doherty
2026-06-16 08:47:19 -04:00
parent 0926ce4dda
commit e89604298d
4 changed files with 78 additions and 6 deletions
@@ -4,8 +4,8 @@
"branch": "feature/disable-login",
"tasks": [
{"id": 62, "ref": "DL-1", "subject": "AuthDisableLoginOptions + Roles.All", "class": "small", "status": "completed", "commits": ["72691e5"]},
{"id": 63, "ref": "DL-2", "subject": "AutoLoginAuthenticationHandler + tests", "class": "high-risk", "status": "completed", "blockedBy": [62]},
{"id": 64, "ref": "DL-3", "subject": "Wire flag into AddSecurity + Host + startup warning", "class": "standard", "status": "pending", "blockedBy": [62, 63]},
{"id": 63, "ref": "DL-2", "subject": "AutoLoginAuthenticationHandler + tests", "class": "high-risk", "status": "completed", "blockedBy": [62], "commits": ["dcd445a", "0926ce4"]},
{"id": 64, "ref": "DL-3", "subject": "Wire flag into AddSecurity + Host + startup warning", "class": "standard", "status": "completed", "blockedBy": [62, 63]},
{"id": 65, "ref": "DL-4", "subject": "Docs + dev config note", "class": "trivial", "status": "pending", "blockedBy": [64]}
],
"lastUpdated": "2026-06-16"
+9 -1
View File
@@ -121,7 +121,15 @@ try
builder.Services.AddZbLdapAuth(
builder.Configuration,
ZB.MOM.WW.ScadaBridge.Security.ServiceCollectionExtensions.LdapSectionPath);
builder.Services.AddSecurity();
// Dev disable-login flag (config-coupled, so read + bound here at the composition root,
// mirroring AddZbLdapAuth). Default false. See AuthDisableLoginOptions / disable-login design doc.
builder.Services.AddOptions<ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions>()
.Bind(builder.Configuration.GetSection(
ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions.SectionName));
var disableLogin = builder.Configuration
.GetSection(ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions.SectionName)
.GetValue<bool>(nameof(ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions.DisableLogin));
builder.Services.AddSecurity(disableLogin);
builder.Services.AddCentralUI();
builder.Services.AddInboundAPI();
@@ -36,8 +36,15 @@ public static class ServiceCollectionExtensions
/// <c>IConfiguration</c>) and component libraries must not accept <c>IConfiguration</c>.
/// </remarks>
/// <param name="services">The service collection to register into.</param>
/// <param name="disableLogin">
/// Dev/test flag (bound from <see cref="Auth.AuthDisableLoginOptions"/> by the Host
/// composition root). When true the cookie handler is replaced — under the same cookie
/// scheme name — by an always-succeeding <see cref="Auth.AutoLoginAuthenticationHandler"/>
/// that authenticates every request as the configured dev user with ALL roles, and a loud
/// startup warning is emitted. Default false. Never enable in production.
/// </param>
/// <returns>The same <paramref name="services"/> instance for chaining.</returns>
public static IServiceCollection AddSecurity(this IServiceCollection services)
public static IServiceCollection AddSecurity(this IServiceCollection services, bool disableLogin = false)
{
// 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
@@ -104,8 +111,27 @@ public static class ServiceCollectionExtensions
// timeout, HTTPS policy) are applied via the SecurityOptions-bound PostConfigure
// below (cookie name through SecurityOptions.CookieName, the rest through
// ZbCookieDefaults.Apply).
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
var authBuilder = services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
if (disableLogin)
{
// DEV/TEST ONLY: replace the cookie handler with an always-succeeding handler registered
// UNDER the cookie scheme name, so every authorization policy (which names this scheme)
// authenticates through it with all roles — zero policy changes. No cookie is written;
// OnValidatePrincipal (idle/refresh) does not apply in this mode. See AuthDisableLoginOptions.
authBuilder.AddScheme<AuthenticationSchemeOptions, Auth.AutoLoginAuthenticationHandler>(
CookieAuthenticationDefaults.AuthenticationScheme, _ => { });
services.AddOptions<Auth.AuthDisableLoginOptions>()
.PostConfigure<ILoggerFactory>((opts, lf) =>
lf.CreateLogger("ZB.MOM.WW.ScadaBridge.Security").LogWarning(
"AUTH DISABLED (ScadaBridge:Security:Auth:DisableLogin=true) — every request is " +
"authenticated as '{User}' with FULL permissions ({Roles}) across ALL sites. This " +
"is a SCADA control surface; dev/test ONLY — never enable in production.",
opts.User, string.Join(",", Roles.All)));
}
else
{
authBuilder.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/auth/logout";
@@ -126,6 +152,7 @@ public static class ServiceCollectionExtensions
// keeps the session (mirrors "LDAP failure: active sessions continue").
options.Events.OnValidatePrincipal = OnValidatePrincipalAsync;
});
}
// CentralUI-005: configure the cookie session as a sliding window so the
// code matches the documented policy ("sliding refresh, 30-minute idle
@@ -147,6 +174,8 @@ public static class ServiceCollectionExtensions
// 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.
// Harmless/unused when disableLogin is true: the cookie handler is not registered,
// so this CookieAuthenticationOptions PostConfigure has no scheme to configure.
services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
.Configure<IOptions<SecurityOptions>, ILoggerFactory>((cookieOptions, securityOptions, loggerFactory) =>
{
@@ -0,0 +1,35 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.DependencyInjection;
using ZB.MOM.WW.ScadaBridge.Security;
using ZB.MOM.WW.ScadaBridge.Security.Auth;
using Xunit;
namespace ZB.MOM.WW.ScadaBridge.Security.Tests;
public class DisableLoginRegistrationTests
{
private static async Task<AuthenticationScheme?> ResolveCookieSchemeAsync(bool disableLogin)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddSecurity(disableLogin);
await using var sp = services.BuildServiceProvider();
var provider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
return await provider.GetSchemeAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
[Fact]
public async Task FlagTrue_RegistersAutoLoginHandlerUnderCookieScheme()
{
var scheme = await ResolveCookieSchemeAsync(disableLogin: true);
Assert.Equal(typeof(AutoLoginAuthenticationHandler), scheme!.HandlerType);
}
[Fact]
public async Task FlagFalse_RegistersCookieHandler()
{
var scheme = await ResolveCookieSchemeAsync(disableLogin: false);
Assert.Equal(typeof(CookieAuthenticationHandler), scheme!.HandlerType);
}
}