feat(security): CookieAuthenticationStateProvider for Blazor circuit expiry detection
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Blazor Server <see cref="AuthenticationStateProvider"/> that snapshots the cookie-backed
|
||||
/// principal supplied at circuit boot and polls <c>/auth/ping</c> every 60 seconds to detect
|
||||
/// expiry. Mirrors ScadaLink's CentralUI implementation.
|
||||
/// </summary>
|
||||
public sealed class CookieAuthenticationStateProvider : AuthenticationStateProvider, IAsyncDisposable
|
||||
{
|
||||
private static readonly TimeSpan PingInterval = TimeSpan.FromSeconds(60);
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly ILogger<CookieAuthenticationStateProvider> _logger;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private ClaimsPrincipal _current;
|
||||
private Task? _pingLoop;
|
||||
|
||||
public CookieAuthenticationStateProvider(
|
||||
ClaimsPrincipal initial,
|
||||
HttpClient http,
|
||||
ILogger<CookieAuthenticationStateProvider> logger)
|
||||
{
|
||||
_current = initial;
|
||||
_http = http;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Task<AuthenticationState> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user