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;
///
/// Resolves from the real DI container at runtime so the bearer
/// pipeline's stay in
/// lock-step with . Replaces the prior
/// services.BuildServiceProvider() antipattern (ASP0000) that built a captive provider
/// from inside .AddJwtBearer.
///
internal sealed class ConfigureJwtBearerFromTokenService(JwtTokenService tokenService)
: IPostConfigureOptions
{
public void PostConfigure(string? name, JwtBearerOptions options)
{
if (name != JwtBearerDefaults.AuthenticationScheme) return;
options.TokenValidationParameters = tokenService.BuildValidationParameters();
}
}
public static class ServiceCollectionExtensions
{
///
/// 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.
///
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();
services.AddScoped();
services.AddDataProtection()
.PersistKeysToDbContext()
.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, _ => { /* parameters set by IPostConfigureOptions below */ });
services.AddSingleton, ConfigureJwtBearerFromTokenService>();
services.AddAuthorization(o =>
{
o.FallbackPolicy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
return services;
}
}