feat(auth): ScadaBridge full canonical claims (ZbClaimTypes role/scope) + ZbCookieDefaults, keep cookie name (Task 1.5)

This commit is contained in:
Joseph Doherty
2026-06-02 06:23:15 -04:00
parent afa55981d5
commit a0938f708b
25 changed files with 247 additions and 50 deletions
@@ -4,6 +4,7 @@ using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using ZB.MOM.WW.Auth.AspNetCore;
namespace ZB.MOM.WW.ScadaBridge.Security;
@@ -12,10 +13,20 @@ public class JwtTokenService
private readonly SecurityOptions _options;
private readonly ILogger<JwtTokenService> _logger;
public const string DisplayNameClaimType = "DisplayName";
public const string UsernameClaimType = "Username";
public const string RoleClaimType = "Role";
public const string SiteIdClaimType = "SiteId";
// Task 1.5 (full canonical claims): these constants are the single source of
// truth that every mint site, every authorization policy, and the token
// validation parameters reference. Redefining them as aliases of the shared
// ZbClaimTypes vocabulary migrates the whole module centrally:
// - Role → the framework URI (ClaimTypes.Role) so [Authorize(Roles=…)],
// IsInRole, AND RequireClaim(RoleClaimType,…) all resolve.
// - SiteId → ZbClaimTypes.ScopeId ("zb:scopeid") — the canonical scope claim.
// - DisplayName/Username → the canonical "zb:" strings.
// LastActivity has no canonical equivalent (it is a ScadaBridge-internal
// idle-timeout anchor), so it keeps its existing literal.
public const string DisplayNameClaimType = ZbClaimTypes.DisplayName;
public const string UsernameClaimType = ZbClaimTypes.Username;
public const string RoleClaimType = ZbClaimTypes.Role;
public const string SiteIdClaimType = ZbClaimTypes.ScopeId;
public const string LastActivityClaimType = "LastActivity";
/// <summary>
@@ -100,7 +111,19 @@ public class JwtTokenService
expires: DateTime.UtcNow.AddMinutes(_options.JwtExpiryMinutes),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
// MapOutboundClaims=false: write the claim TYPE strings into the JWT
// verbatim (e.g. the ClaimTypes.Role URI for RoleClaimType) rather than
// letting JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap rewrite the
// framework URI to a short JWT name ("role"). Paired with
// MapInboundClaims=false on validation (see ValidateToken), this makes the
// claim type the policy checks — RequireClaim(RoleClaimType, …) — byte-for-byte
// the same string both in the token and after it is read back, with no
// mapping round-trip surprises. The "zb:" scope/display/username claims are
// not in any map and were unaffected either way; pinning both directions
// makes the role/site migration to canonical types deterministic.
var handler = new JwtSecurityTokenHandler { MapInboundClaims = false };
handler.OutboundClaimTypeMap.Clear();
return handler.WriteToken(token);
}
/// <summary>Validates a JWT string and returns the decoded <see cref="ClaimsPrincipal"/>, or null if the token is invalid or expired.</summary>
@@ -118,12 +141,26 @@ public class JwtTokenService
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = key,
ClockSkew = TimeSpan.Zero
ClockSkew = TimeSpan.Zero,
// The token was minted carrying the canonical claim TYPE strings
// verbatim (RoleClaimType = ClaimTypes.Role URI, SiteIdClaimType =
// ZbClaimTypes.ScopeId, etc.). Pin the same role/name claim types here
// so the validated principal's Identity.Name and IsInRole resolve, and
// so RequireClaim(RoleClaimType, …) sees exactly the type that is in the
// token.
RoleClaimType = RoleClaimType,
NameClaimType = ZbClaimTypes.Name
};
try
{
var handler = new JwtSecurityTokenHandler();
// MapInboundClaims=false: do NOT let JwtSecurityTokenHandler rewrite
// inbound claim types via DefaultInboundClaimTypeMap. The token already
// holds the canonical type strings (see GenerateToken's
// MapOutboundClaims=false), so reading them back unmapped yields exactly
// the strings every policy + claim helper expects.
var handler = new JwtSecurityTokenHandler { MapInboundClaims = false };
var principal = handler.ValidateToken(token, validationParameters, out _);
return principal;
}