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