feat(security): cookie+JWT hybrid auth via AddOtOpcUaAuth

This commit is contained in:
Joseph Doherty
2026-05-26 04:35:48 -04:00
parent 93316e3431
commit 207fc6aba9
3 changed files with 87 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("ZB.MOM.WW.OtOpcUa.Security.Tests")]

View File

@@ -0,0 +1,11 @@
namespace ZB.MOM.WW.OtOpcUa.Security;
public sealed class OtOpcUaCookieOptions
{
public const string SectionName = "Security:Cookie";
public string Name { get; set; } = "OtOpcUa.Auth";
/// <summary>Idle sliding window, in minutes (default 30).</summary>
public int ExpiryMinutes { get; set; } = 30;
}

View File

@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
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;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Wires cookie+JWT hybrid authentication. Cookies are the primary scheme for browser-facing
/// Blazor + Razor flows; JWT bearer is layered in for external API consumers (OPC UA client
/// tools, scripts). DataProtection keys persist to the shared ConfigDb so cookies survive
/// failover between nodes.
/// </summary>
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>();
services.AddScoped<ILdapAuthService, LdapAuthService>();
services.AddDataProtection()
.PersistKeysToDbContext<OtOpcUaConfigDbContext>()
.SetApplicationName("OtOpcUa");
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(o =>
{
o.Cookie.Name = "OtOpcUa.Auth";
o.Cookie.HttpOnly = true;
o.Cookie.SameSite = SameSiteMode.Strict;
o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
o.SlidingExpiration = true;
o.ExpireTimeSpan = TimeSpan.FromMinutes(30);
o.Events.OnRedirectToLogin = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status401Unauthorized;
return Task.CompletedTask;
};
o.Events.OnRedirectToAccessDenied = ctx =>
{
ctx.Response.StatusCode = StatusCodes.Status403Forbidden;
return Task.CompletedTask;
};
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o =>
{
using var scope = services.BuildServiceProvider().CreateScope();
var jwt = scope.ServiceProvider.GetRequiredService<JwtTokenService>();
o.TokenValidationParameters = jwt.BuildValidationParameters();
});
services.AddAuthorization(o =>
{
o.FallbackPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
return services;
}
}