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; } }