using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace ZB.MOM.WW.ScadaBridge.Security;
///
/// The single, shared source of truth for the FULL set of claims that
/// back an interactive cookie session. BOTH the /auth/login endpoint and the
/// OnValidatePrincipal mid-session role-refresh path build their principal
/// through , so the two can never drift — the spec requires the
/// refresh to "rebuild claims identically to /auth/login".
///
///
/// The claim shape is exactly what the login endpoint historically minted, plus the
/// two additions:
///
/// - — resolves Identity.Name.
/// - — human display name.
/// - — canonical username.
/// - — one per mapped role.
/// - — one per permitted site,
/// ONLY when the mapping is not system-wide (deny-by-omission preserved).
/// - — one per raw LDAP group
/// — the durable input the mid-session RoleMapper re-run consumes.
/// - — the role-mapping
/// refresh anchor, ISO-8601 round-trippable.
/// - — the idle-timeout
/// anchor; seeded to the refresh timestamp at login so idle-timeout can be
/// enforced consistently from the very first request.
///
/// The is built with nameType = ClaimTypes.Name
/// and roleType = RoleClaimType so Identity.Name / IsInRole /
/// [Authorize(Roles=…)] resolve against exactly the canonical types minted here.
///
public static class SessionClaimBuilder
{
///
/// Builds the full cookie-session from the resolved
/// identity, the raw LDAP groups, the DB-backed role mapping, and the refresh
/// timestamp. Used identically by /auth/login and the
/// OnValidatePrincipal refresh path so the two cannot diverge.
///
/// The canonical authenticated username (becomes + ).
/// The human-readable display name.
/// The user's raw LDAP groups, stored one per claim.
/// The DB-backed role mapping (roles + permitted sites + system-wide flag).
/// The role-mapping refresh anchor; also seeds the last-activity anchor.
/// The authentication type stamped on the identity (defaults to the cookie scheme).
/// A fully populated cookie .
public static ClaimsPrincipal Build(
string username,
string displayName,
IReadOnlyList groups,
RoleMappingResult mapping,
DateTimeOffset refreshTimestamp,
string authenticationType = CookieAuthenticationDefaults.AuthenticationScheme)
{
ArgumentNullException.ThrowIfNull(username);
ArgumentNullException.ThrowIfNull(displayName);
ArgumentNullException.ThrowIfNull(groups);
ArgumentNullException.ThrowIfNull(mapping);
var refreshStamp = refreshTimestamp.ToString("o");
var claims = new List
{
new(ClaimTypes.Name, username),
new(JwtTokenService.DisplayNameClaimType, displayName),
new(JwtTokenService.UsernameClaimType, username),
// Role-refresh anchor AND idle anchor are seeded from the same instant at
// build time. They then diverge: OnValidatePrincipal advances LastActivity
// on every request but only advances LastRoleRefresh when it actually
// re-runs the mapping.
new(JwtTokenService.LastRoleRefreshClaimType, refreshStamp),
new(JwtTokenService.LastActivityClaimType, refreshStamp),
};
foreach (var role in mapping.Roles)
{
claims.Add(new Claim(JwtTokenService.RoleClaimType, role));
}
// Deny-by-omission: only stamp SiteId claims for a non-system-wide mapping.
if (!mapping.IsSystemWideDeployment)
{
foreach (var siteId in mapping.PermittedSiteIds)
{
claims.Add(new Claim(JwtTokenService.SiteIdClaimType, siteId));
}
}
// Store the raw LDAP groups so the mid-session refresh can re-run the
// DB-backed RoleMapper without any LDAP round-trip.
foreach (var group in groups)
{
claims.Add(new Claim(JwtTokenService.GroupClaimType, group));
}
var identity = new ClaimsIdentity(
claims,
authenticationType: authenticationType,
nameType: ClaimTypes.Name,
roleType: JwtTokenService.RoleClaimType);
return new ClaimsPrincipal(identity);
}
/// Reads the stored LDAP group claims () off a principal.
/// The cookie principal to read from.
/// The stored LDAP group names; empty if none were stored.
public static IReadOnlyList ReadGroups(ClaimsPrincipal principal)
{
ArgumentNullException.ThrowIfNull(principal);
return principal.FindAll(JwtTokenService.GroupClaimType).Select(c => c.Value).ToList();
}
}