Files
lux/Lux.UI/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs
2025-07-18 08:39:10 +02:00

111 lines
4.4 KiB
C#

using Lux.UI.Client;
using Lux.UI.Data;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Security.Claims;
namespace Lux.UI.Components.Account
{
// This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user
// every 30 minutes an interactive circuit is connected. It also uses PersistentComponentState to flow the
// authentication state to the client which is then fixed for the lifetime of the WebAssembly application.
internal sealed class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly IServiceScopeFactory scopeFactory;
private readonly PersistentComponentState state;
private readonly IdentityOptions options;
private readonly PersistingComponentStateSubscription subscription;
private Task<AuthenticationState>? authenticationStateTask;
public PersistingRevalidatingAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory serviceScopeFactory,
PersistentComponentState persistentComponentState,
IOptions<IdentityOptions> optionsAccessor)
: base(loggerFactory)
{
scopeFactory = serviceScopeFactory;
state = persistentComponentState;
options = optionsAccessor.Value;
AuthenticationStateChanged += OnAuthenticationStateChanged;
subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly);
}
protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30);
protected override async Task<bool> ValidateAuthenticationStateAsync(
AuthenticationState authenticationState, CancellationToken cancellationToken)
{
// Get the user manager from a new scope to ensure it fetches fresh data
await using var scope = scopeFactory.CreateAsyncScope();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<ApplicationUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user is null)
{
return false;
}
else if (!userManager.SupportsUserSecurityStamp)
{
return true;
}
else
{
var principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType);
var userStamp = await userManager.GetSecurityStampAsync(user);
return principalStamp == userStamp;
}
}
private void OnAuthenticationStateChanged(Task<AuthenticationState> task)
{
authenticationStateTask = task;
}
private async Task OnPersistingAsync()
{
if (authenticationStateTask is null)
{
throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}().");
}
var authenticationState = await authenticationStateTask;
var principal = authenticationState.User;
if (principal.Identity?.IsAuthenticated == true)
{
var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value;
var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value;
if (userId != null && email != null)
{
state.PersistAsJson(nameof(UserInfo), new UserInfo
{
UserId = userId,
Email = email,
});
}
}
}
protected override void Dispose(bool disposing)
{
subscription.Dispose();
AuthenticationStateChanged -= OnAuthenticationStateChanged;
base.Dispose(disposing);
}
}
}