fix(central-ui): resolve CentralUI-005 — sliding cookie session expiry (Security AddCookie + AuthEndpoints + SessionExpiry)
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
| Last reviewed | 2026-05-16 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `9c60592` |
|
||||
| Open findings | 2 |
|
||||
| Open findings | 1 |
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -248,7 +248,7 @@ Fixed by the commit whose message references `CentralUI-004`.
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
| Location | `src/ScadaLink.CentralUI/Auth/AuthEndpoints.cs:47-81`; `src/ScadaLink.CentralUI/Components/Shared/SessionExpiry.razor:18-30` |
|
||||
|
||||
**Description**
|
||||
@@ -269,17 +269,48 @@ fixed 30-minute model. The code and the documented decision must agree.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved — requires a cross-module change plus a design decision, both out of
|
||||
scope for a CentralUI-only fix._ Verified 2026-05-16: the discrepancy is real.
|
||||
The sliding-expiration mechanism, however, is owned by the cookie
|
||||
authentication middleware configured in **`ScadaLink.Security`**
|
||||
(`ServiceCollectionExtensions.AddCookie` — currently sets neither
|
||||
`ExpireTimeSpan` nor `SlidingExpiration`); `AuthEndpoints` (CentralUI) only sets
|
||||
the absolute `ExpiresUtc`/`expires_at`. Implementing "15-minute sliding token"
|
||||
means editing `ScadaLink.Security`, which this module's review cannot touch, and
|
||||
the alternative — amending the documented decision to a fixed 30-minute model —
|
||||
is a design decision, not a code fix. Left Open and surfaced for a follow-up
|
||||
that spans CentralUI + Security, or a design-doc amendment.
|
||||
Resolved 2026-05-16 (commit `<pending>`) — cross-module fix (CentralUI +
|
||||
Security), explicitly authorized. Root cause confirmed against the source:
|
||||
`AddCookie` (`ScadaLink.Security/ServiceCollectionExtensions.cs`) set neither
|
||||
`ExpireTimeSpan` nor `SlidingExpiration`; `AuthEndpoints` stamped a fixed
|
||||
`expires_at = UtcNow + 30 min` claim and a 30-minute absolute cookie
|
||||
`ExpiresUtc`; `SessionExpiry.razor` scheduled one hard redirect at that fixed
|
||||
instant — a hard 30-minute cap, no sliding renewal, no 15-minute component.
|
||||
|
||||
**What was implemented — sliding session window.** ASP.NET Core cookie
|
||||
authentication exposes a single `ExpireTimeSpan` plus a `SlidingExpiration`
|
||||
flag; it cannot natively model *both* a 15-minute sliding token *and* a separate
|
||||
30-minute absolute idle cap. The faithful interpretation implemented: the cookie
|
||||
session window **is** the idle timeout. `AddSecurity` now post-configures the
|
||||
cookie options with `ExpireTimeSpan = TimeSpan.FromMinutes(SecurityOptions.IdleTimeoutMinutes)`
|
||||
(default 30, bound from `appsettings` via the existing options pattern, not
|
||||
hard-coded) and `SlidingExpiration = true`. The middleware therefore re-issues
|
||||
the cookie on activity once past the halfway mark of the window: an active user
|
||||
is continually renewed, an idle user is signed out after the 30-minute idle
|
||||
timeout — exactly the documented "sliding refresh, 30-minute idle timeout". The
|
||||
separate 15-minute `JwtExpiryMinutes` governs the lifetime of the *embedded JWT*
|
||||
itself (`JwtTokenService`) — a distinct layer from the cookie session window;
|
||||
it is not, and per the ASP.NET cookie model cannot be, a second independent
|
||||
sliding window inside the same cookie. `AuthEndpoints` no longer imposes a
|
||||
contradictory absolute cap: the `expires_at` claim and the manual cookie
|
||||
`ExpiresUtc` were removed, and a new `BuildSignInProperties()` helper sets only
|
||||
`IsPersistent = true` (no `ExpiresUtc`, `AllowRefresh` left unset) so the
|
||||
middleware owns expiry. `SessionExpiry.razor` no longer reads a fixed
|
||||
login-time deadline (the `expires_at` claim is gone) and no longer hard-redirects
|
||||
at a fixed instant: it now polls the authentication state on a recurring
|
||||
interval and redirects to `/login` only once the sliding cookie has actually
|
||||
lapsed server-side — so an active user is never logged out mid-session.
|
||||
|
||||
Regression tests fail against the pre-fix code and pass after. Security:
|
||||
`AddSecurity_AuthCookie_UsesSlidingExpiration`,
|
||||
`AddSecurity_AuthCookie_ExpireTimeSpanMatchesIdleTimeout` (pre-fix
|
||||
`ExpireTimeSpan` was the 14-day default — confirmed failing), and
|
||||
`AddSecurity_AuthCookie_ExpireTimeSpanIsConfigurable` (pins the options-pattern
|
||||
binding). CentralUI: `SessionExpiryPolicyTests.BuildSignInProperties_DoesNotSetFixedAbsoluteExpiry`,
|
||||
`..._IsPersistent`, `..._AllowsSlidingRefresh` pin that the login sign-in no
|
||||
longer imposes a fixed absolute cap. `dotnet build ScadaLink.slnx` clean;
|
||||
`tests/ScadaLink.Security.Tests` 57 passed, `tests/ScadaLink.CentralUI.Tests`
|
||||
254 passed.
|
||||
|
||||
### CentralUI-006 — Deployment status page polls every 10s despite the documented SignalR-push design
|
||||
|
||||
|
||||
Reference in New Issue
Block a user