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