feat(security): cookie session idle-timeout + LDAP-free role-mapping refresh (#15, M2.19)
Spike outcome: the shared ILdapAuthService (ZB.MOM.WW.Auth.Abstractions, an external
NuGet package) exposes ONLY AuthenticateAsync(username, password, ct) — no passwordless
service-account group-search. A live LDAP group re-query for an active session therefore
requires a new lib method and is OUT OF SCOPE (cannot modify the external package).
Implemented the always-achievable layers (cookie-only; no embedded JWT for cookie principals):
- /auth/login now stores the user's raw LDAP groups (one zb:group claim each) plus a
zb:lastrolerefresh anchor (login time, UTC), seeding the LastActivity idle anchor too.
- SessionClaimBuilder: single shared DRY claim-builder used by BOTH /auth/login AND the
refresh path, so the two claim shapes cannot drift (canonical identity/role/scope claims
with nameType/roleType pinned, plus the M2.19 group + refresh-anchor additions).
- CookieSessionValidator (TimeProvider-injected, unit-testable) + a thin
CookieAuthenticationEvents.OnValidatePrincipal adapter:
* idle-timeout: a session past IdleTimeoutMinutes (default 30) is RejectPrincipal+SignOut;
consistent with the cookie ExpireTimeSpan+SlidingExpiration window (same value).
* role refresh WITHOUT LDAP: when older than RoleRefreshThresholdMinutes (new option,
default 15) the DB-backed RoleMapper re-runs on the STORED groups, claims are rebuilt
via the shared builder, the anchor advances, principal is replaced + cookie renewed.
Revoked DB mappings drop the user's roles mid-session.
* fail-soft: any refresh error KEEPS the existing principal (no sign-out, never throws)
— mirrors the documented "LDAP failure: active sessions continue with current roles".
- Documented residual limitation in Component-Security.md: central role-mapping/scope
changes apply within ~15 min without LDAP; live directory group-membership changes are
picked up only at next login (needs a passwordless group-search on the external
ZB.MOM.WW.Auth.Ldap lib — tracked follow-up).
Tests (Security.Tests, all green): CookieSessionValidatorTests + SessionClaimBuilderParityTests
— idle reject/keep, LDAP-free remap-from-stored-groups, revoked-roles loss, sub-threshold
no-refresh, refresh-throws-keeps-session, and login/refresh claim-parity.
This commit is contained in:
@@ -29,6 +29,22 @@ public class JwtTokenService
|
||||
public const string SiteIdClaimType = ZbClaimTypes.ScopeId;
|
||||
public const string LastActivityClaimType = "LastActivity";
|
||||
|
||||
// M2.19 (#15): the cookie session now stores the user's raw LDAP groups and a
|
||||
// role-mapping refresh anchor so an active interactive session can re-run the
|
||||
// DB-backed RoleMapper (NOT LDAP) mid-session and pick up central role-mapping
|
||||
// changes. These two have no canonical ZbClaimTypes equivalent (the shared
|
||||
// vocabulary covers identity/role/scope, not the ScadaBridge-internal refresh
|
||||
// machinery), so they keep "zb:"-prefixed ScadaBridge-local literals:
|
||||
// - GroupClaimType ("zb:group", one per LDAP group) is the input the
|
||||
// mid-session RoleMapper re-run consumes — the groups are the durable
|
||||
// fact; the roles are the derived projection that can go stale.
|
||||
// - LastRoleRefreshClaimType ("zb:lastrolerefresh", ISO-8601 "o") anchors
|
||||
// the role-mapping refresh interval (SecurityOptions.RoleRefreshThresholdMinutes).
|
||||
// LastActivityClaimType (above) remains the idle-timeout anchor — a separate
|
||||
// clock from the role-refresh anchor.
|
||||
public const string GroupClaimType = "zb:group";
|
||||
public const string LastRoleRefreshClaimType = "zb:lastrolerefresh";
|
||||
|
||||
/// <summary>
|
||||
/// Fixed issuer bound into every token and required on validation. Binding
|
||||
/// issuer/audience is defence-in-depth: even though the HMAC key is shared only
|
||||
|
||||
Reference in New Issue
Block a user