diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Blazor/CookieAuthenticationStateProvider.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Blazor/CookieAuthenticationStateProvider.cs
new file mode 100644
index 0000000..6671489
--- /dev/null
+++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Blazor/CookieAuthenticationStateProvider.cs
@@ -0,0 +1,72 @@
+using System.Net.Http.Json;
+using System.Security.Claims;
+using Microsoft.AspNetCore.Components.Authorization;
+using Microsoft.Extensions.Logging;
+
+namespace ZB.MOM.WW.OtOpcUa.Security.Blazor;
+
+///
+/// Blazor Server that snapshots the cookie-backed
+/// principal supplied at circuit boot and polls /auth/ping every 60 seconds to detect
+/// expiry. Mirrors ScadaLink's CentralUI implementation.
+///
+public sealed class CookieAuthenticationStateProvider : AuthenticationStateProvider, IAsyncDisposable
+{
+ private static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(60);
+
+ private readonly HttpClient _http;
+ private readonly ILogger _logger;
+ private readonly CancellationTokenSource _cts = new();
+ private ClaimsPrincipal _current;
+ private Task? _pingLoop;
+
+ public CookieAuthenticationStateProvider(
+ ClaimsPrincipal initial,
+ HttpClient http,
+ ILogger logger)
+ {
+ _current = initial;
+ _http = http;
+ _logger = logger;
+ }
+
+ public override Task GetAuthenticationStateAsync()
+ {
+ _pingLoop ??= Task.Run(() => PingLoopAsync(_cts.Token));
+ return Task.FromResult(new AuthenticationState(_current));
+ }
+
+ private async Task PingLoopAsync(CancellationToken ct)
+ {
+ try
+ {
+ while (!ct.IsCancellationRequested)
+ {
+ await Task.Delay(PingInterval, ct).ConfigureAwait(false);
+ var resp = await _http.GetAsync("/auth/ping", ct).ConfigureAwait(false);
+ if (!resp.IsSuccessStatusCode && _current.Identity?.IsAuthenticated == true)
+ {
+ _logger.LogInformation("/auth/ping returned {Code}; notifying circuit", (int)resp.StatusCode);
+ _current = new ClaimsPrincipal(new ClaimsIdentity());
+ NotifyAuthenticationStateChanged(
+ Task.FromResult(new AuthenticationState(_current)));
+ }
+ }
+ }
+ catch (OperationCanceledException) { /* expected on shutdown */ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Auth ping loop terminated unexpectedly");
+ }
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ _cts.Cancel();
+ if (_pingLoop is not null)
+ {
+ try { await _pingLoop.ConfigureAwait(false); } catch { /* swallow shutdown errors */ }
+ }
+ _cts.Dispose();
+ }
+}