feat(auth): MxGateway dashboard adopt ZbClaimTypes + ZbCookieDefaults, keep cookie name (Task 1.5)

- DashboardAuthenticator.CreatePrincipal: emit ZbClaimTypes.Username ("zb:username") with
  the login username, ZbClaimTypes.DisplayName ("zb:displayname") with the display name,
  ZbClaimTypes.Name (== ClaimTypes.Name) for Identity.Name resolution, ZbClaimTypes.Role
  (== ClaimTypes.Role) for IsInRole/[Authorize]. Keep ClaimTypes.NameIdentifier for back-compat
  read-sites; keep mxgateway:ldap_group unchanged (MxGateway-specific, no ZbClaimType for groups).
  ClaimsIdentity built with nameType=ZbClaimTypes.Name, roleType=ZbClaimTypes.Role.
- DashboardServiceCollectionExtensions.AddGatewayDashboard: route cookie hardening through
  ZbCookieDefaults.Apply(requireHttps:true, idleTimeout:8h); set cookie name/path/redirects
  after Apply; PostConfigure still overrides SecurePolicy per RequireHttpsCookie setting.
- DashboardAuthenticatorTests: add AuthenticateAsync_Success_EmitsCanonicalZbClaims asserting
  zb:username, zb:displayname, ZbClaimTypes.Role per role, Identity.Name, and ldap_group preserved.
This commit is contained in:
Joseph Doherty
2026-06-02 06:10:48 -04:00
parent 05009d7370
commit 7e1af37eb1
3 changed files with 85 additions and 13 deletions
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.Auth.Abstractions.Ldap;
using ZB.MOM.WW.Auth.Abstractions.Roles;
using ZB.MOM.WW.Auth.AspNetCore;
using ZB.MOM.WW.MxGateway.Server.Configuration;
using ZB.MOM.WW.MxGateway.Server.Dashboard;
@@ -125,6 +126,59 @@ public sealed class DashboardAuthenticatorTests
Assert.Equal(["GwAdmin", "GwReader"], groupClaims);
}
/// <summary>
/// Task 1.5: the principal emits canonical ZbClaimTypes.Username ("zb:username") with
/// the login username, ZbClaimTypes.Role (= ClaimTypes.Role) for each resolved role, and
/// ZbClaimTypes.DisplayName ("zb:displayname") with the display name — while keeping
/// ClaimTypes.NameIdentifier, ClaimTypes.Name, and mxgateway:ldap_group claims intact.
/// </summary>
[Fact]
public async Task AuthenticateAsync_Success_EmitsCanonicalZbClaims()
{
FakeLdapAuthService ldap = new(LdapAuthResult.Success(
username: "jsmith",
displayName: "John Smith",
groups: ["GwAdmin", "GwReader"]));
DashboardAuthenticator authenticator = CreateAuthenticator(ldap, StandardMapping());
DashboardAuthenticationResult result = await authenticator.AuthenticateAsync(
"jsmith",
"pw",
CancellationToken.None);
Assert.True(result.Succeeded);
ClaimsPrincipal principal = Assert.IsType<ClaimsPrincipal>(result.Principal);
// ZbClaimTypes.Username ("zb:username") carries the login username.
Assert.Equal("jsmith", principal.FindFirstValue(ZbClaimTypes.Username));
// ZbClaimTypes.DisplayName ("zb:displayname") carries the display name.
Assert.Equal("John Smith", principal.FindFirstValue(ZbClaimTypes.DisplayName));
// ZbClaimTypes.Role (= ClaimTypes.Role) is used for each role claim.
IReadOnlyList<string> roleClaims = principal
.FindAll(ZbClaimTypes.Role)
.Select(c => c.Value)
.OrderBy(r => r)
.ToList();
Assert.Contains(DashboardRoles.Admin, roleClaims);
Assert.Contains(DashboardRoles.Viewer, roleClaims);
// IsInRole still works (identity built with roleType = ZbClaimTypes.Role).
Assert.True(principal.IsInRole(DashboardRoles.Admin));
Assert.True(principal.IsInRole(DashboardRoles.Viewer));
// ClaimTypes.Name still resolves as Identity.Name (nameType = ZbClaimTypes.Name = ClaimTypes.Name).
Assert.Equal("John Smith", principal.Identity?.Name);
// mxgateway:ldap_group claims are preserved.
IReadOnlyList<string> groups = principal
.FindAll(DashboardAuthenticationDefaults.LdapGroupClaimType)
.Select(c => c.Value)
.ToList();
Assert.Equal(["GwAdmin", "GwReader"], groups);
}
/// <summary>
/// When the user authenticates but none of their groups map to a dashboard role,
/// the login is denied (the long-standing "no roles matched → denied" rule).