// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components.Server { /// /// A base class for services that receive an /// authentication state from the host environment, and revalidate it at regular intervals. /// public abstract class RevalidatingServerAuthenticationStateProvider : ServerAuthenticationStateProvider, IDisposable { private readonly ILogger _logger; private CancellationTokenSource _loopCancellationTokenSource = new CancellationTokenSource(); /// /// Constructs an instance of . /// /// A logger factory. public RevalidatingServerAuthenticationStateProvider(ILoggerFactory loggerFactory) { if (loggerFactory is null) { throw new ArgumentNullException(nameof(loggerFactory)); } _logger = loggerFactory.CreateLogger(); // Whenever we receive notification of a new authentication state, cancel any // existing revalidation loop and start a new one AuthenticationStateChanged += authenticationStateTask => { _loopCancellationTokenSource?.Cancel(); _loopCancellationTokenSource = new CancellationTokenSource(); _ = RevalidationLoop(authenticationStateTask, _loopCancellationTokenSource.Token); }; } /// /// Gets the interval between revalidation attempts. /// protected abstract TimeSpan RevalidationInterval { get; } /// /// Determines whether the authentication state is still valid. /// /// The current . /// A to observe while performing the operation. /// A that resolves as true if the is still valid, or false if it is not. protected abstract Task ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken); private async Task RevalidationLoop(Task authenticationStateTask, CancellationToken cancellationToken) { try { var authenticationState = await authenticationStateTask; if (authenticationState.User.Identity.IsAuthenticated) { while (!cancellationToken.IsCancellationRequested) { bool isValid; try { await Task.Delay(RevalidationInterval, cancellationToken); isValid = await ValidateAuthenticationStateAsync(authenticationState, cancellationToken); } catch (TaskCanceledException tce) { // If it was our cancellation token, then this revalidation loop gracefully completes // Otherwise, treat it like any other failure if (tce.CancellationToken == cancellationToken) { break; } throw; } if (!isValid) { ForceSignOut(); break; } } } } catch (Exception ex) { _logger.LogError(ex, "An error occurred while revalidating authentication state"); ForceSignOut(); } } private void ForceSignOut() { var anonymousUser = new ClaimsPrincipal(new ClaimsIdentity()); var anonymousState = new AuthenticationState(anonymousUser); SetAuthenticationState(Task.FromResult(anonymousState)); } void IDisposable.Dispose() { _loopCancellationTokenSource?.Cancel(); Dispose(disposing: true); } /// protected virtual void Dispose(bool disposing) { } } }