Fix Central launch profile: auth middleware, cookie auth, antiforgery, static files
- Add UseAuthentication/UseAuthorization/UseAntiforgery/UseStaticFiles middleware - Register ASP.NET Core cookie authentication scheme in AddSecurity() - Update auth endpoints to use SignInAsync/SignOutAsync (proper cookie auth) - Add [AllowAnonymous] to login page - Create wwwroot for static file serving - Regenerate clean EF migration after model changes Verified with launch profile "ScadaLink Central": - Host starts, connects to SQL Server, applies EF migrations - Akka.NET cluster forms (remoting on 8081, node joins self as leader) - /health/ready returns Healthy (DB + Akka checks) - LDAP auth works (admin/password via GLAuth → 302 + auth cookie set) - Login page renders (HTTP 200) - Unauthenticated requests redirect to /login
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Routing;
|
using Microsoft.AspNetCore.Routing;
|
||||||
@@ -8,7 +11,7 @@ namespace ScadaLink.CentralUI.Auth;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Minimal API endpoints for login/logout. These run outside Blazor Server (standard HTTP POST).
|
/// Minimal API endpoints for login/logout. These run outside Blazor Server (standard HTTP POST).
|
||||||
/// On success, sets an HTTP-only cookie containing the JWT, then redirects to dashboard.
|
/// On success, signs in via ASP.NET Core cookie authentication and redirects to dashboard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AuthEndpoints
|
public static class AuthEndpoints
|
||||||
{
|
{
|
||||||
@@ -41,37 +44,47 @@ public static class AuthEndpoints
|
|||||||
// Map LDAP groups to roles
|
// Map LDAP groups to roles
|
||||||
var roleMappingResult = await roleMapper.MapGroupsToRolesAsync(authResult.Groups ?? []);
|
var roleMappingResult = await roleMapper.MapGroupsToRolesAsync(authResult.Groups ?? []);
|
||||||
|
|
||||||
var token = jwtService.GenerateToken(
|
// Build claims from LDAP auth + role mapping
|
||||||
authResult.DisplayName ?? username,
|
var claims = new List<Claim>
|
||||||
authResult.Username ?? username,
|
{
|
||||||
roleMappingResult.Roles,
|
new(ClaimTypes.Name, authResult.Username ?? username),
|
||||||
roleMappingResult.IsSystemWideDeployment ? null : roleMappingResult.PermittedSiteIds);
|
new(JwtTokenService.DisplayNameClaimType, authResult.DisplayName ?? username),
|
||||||
|
new(JwtTokenService.UsernameClaimType, authResult.Username ?? username),
|
||||||
|
};
|
||||||
|
|
||||||
// Set HTTP-only cookie with the JWT
|
foreach (var role in roleMappingResult.Roles)
|
||||||
context.Response.Cookies.Append(
|
{
|
||||||
CookieAuthenticationStateProvider.AuthCookieName,
|
claims.Add(new Claim(JwtTokenService.RoleClaimType, role));
|
||||||
token,
|
}
|
||||||
new CookieOptions
|
|
||||||
|
if (!roleMappingResult.IsSystemWideDeployment)
|
||||||
|
{
|
||||||
|
foreach (var siteId in roleMappingResult.PermittedSiteIds)
|
||||||
{
|
{
|
||||||
HttpOnly = true,
|
claims.Add(new Claim(JwtTokenService.SiteIdClaimType, siteId));
|
||||||
Secure = context.Request.IsHttps,
|
}
|
||||||
SameSite = SameSiteMode.Strict,
|
}
|
||||||
Path = "/",
|
|
||||||
// Cookie expiry matches JWT idle timeout (30 min default)
|
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
MaxAge = TimeSpan.FromMinutes(30)
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
|
await context.SignInAsync(
|
||||||
|
CookieAuthenticationDefaults.AuthenticationScheme,
|
||||||
|
principal,
|
||||||
|
new AuthenticationProperties
|
||||||
|
{
|
||||||
|
IsPersistent = true,
|
||||||
|
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30)
|
||||||
});
|
});
|
||||||
|
|
||||||
context.Response.Redirect("/");
|
context.Response.Redirect("/");
|
||||||
});
|
}).DisableAntiforgery();
|
||||||
|
|
||||||
endpoints.MapPost("/auth/logout", (HttpContext context) =>
|
endpoints.MapPost("/auth/logout", async (HttpContext context) =>
|
||||||
{
|
{
|
||||||
context.Response.Cookies.Delete(CookieAuthenticationStateProvider.AuthCookieName, new CookieOptions
|
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
{
|
|
||||||
Path = "/"
|
|
||||||
});
|
|
||||||
context.Response.Redirect("/login");
|
context.Response.Redirect("/login");
|
||||||
});
|
}).DisableAntiforgery();
|
||||||
|
|
||||||
return endpoints;
|
return endpoints;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
@page "/login"
|
@page "/login"
|
||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@attribute [AllowAnonymous]
|
||||||
|
|
||||||
<div class="container" style="max-width: 400px; margin-top: 10vh;">
|
<div class="container" style="max-width: 400px; margin-top: 10vh;">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ try
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Middleware pipeline
|
||||||
|
app.UseStaticFiles();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.UseAntiforgery();
|
||||||
|
|
||||||
// WP-12: Map readiness endpoint — returns 503 until all checks pass, 200 when ready
|
// WP-12: Map readiness endpoint — returns 503 until all checks pass, 200 when ready
|
||||||
app.MapHealthChecks("/health/ready", new HealthCheckOptions
|
app.MapHealthChecks("/health/ready", new HealthCheckOptions
|
||||||
{
|
{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
0
src/ScadaLink.Host/wwwroot/.gitkeep
Normal file
0
src/ScadaLink.Host/wwwroot/.gitkeep
Normal file
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace ScadaLink.Security;
|
namespace ScadaLink.Security;
|
||||||
@@ -9,6 +10,18 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<LdapAuthService>();
|
services.AddScoped<LdapAuthService>();
|
||||||
services.AddScoped<JwtTokenService>();
|
services.AddScoped<JwtTokenService>();
|
||||||
services.AddScoped<RoleMapper>();
|
services.AddScoped<RoleMapper>();
|
||||||
|
|
||||||
|
// Register ASP.NET Core authentication with cookie scheme
|
||||||
|
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
|
.AddCookie(options =>
|
||||||
|
{
|
||||||
|
options.LoginPath = "/login";
|
||||||
|
options.LogoutPath = "/auth/logout";
|
||||||
|
options.Cookie.Name = "ScadaLink.Auth";
|
||||||
|
options.Cookie.HttpOnly = true;
|
||||||
|
options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
|
||||||
|
});
|
||||||
|
|
||||||
services.AddScadaLinkAuthorization();
|
services.AddScadaLinkAuthorization();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
Reference in New Issue
Block a user