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? authenticationStateTask; public PersistingRevalidatingAuthenticationStateProvider( ILoggerFactory loggerFactory, IServiceScopeFactory serviceScopeFactory, PersistentComponentState persistentComponentState, IOptions 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 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>(); return await ValidateSecurityStampAsync(userManager, authenticationState.User); } private async Task ValidateSecurityStampAsync(UserManager 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 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); } } }