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