From d1146d043fff4889041b423c6ebc1bf1ef8e4bd6 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 28 Jun 2019 09:12:59 +0100 Subject: [PATCH] Add authentication revalidation logic to IndividualLocalAuth server-side Blazor template. Implements #10698 (#11548) --- ...RevalidatingAuthenticationStateProvider.cs | 98 +++++++++++++++++++ .../RazorComponentsWeb-CSharp/Startup.cs | 5 + .../test/template-baselines.json | 1 + 3 files changed, 104 insertions(+) create mode 100644 src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Areas/Identity/RevalidatingAuthenticationStateProvider.cs diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Areas/Identity/RevalidatingAuthenticationStateProvider.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Areas/Identity/RevalidatingAuthenticationStateProvider.cs new file mode 100644 index 0000000000..43932f618b --- /dev/null +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Areas/Identity/RevalidatingAuthenticationStateProvider.cs @@ -0,0 +1,98 @@ +using System; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace RazorComponentsWeb_CSharp.Areas.Identity +{ + /// + /// An service that revalidates the + /// authentication state at regular intervals. If a signed-in user's security + /// stamp changes, this revalidation mechanism will sign the user out. + /// + /// The type encapsulating a user. + public class RevalidatingAuthenticationStateProvider + : AuthenticationStateProvider, IDisposable where TUser : class + { + private readonly static TimeSpan RevalidationInterval = TimeSpan.FromMinutes(30); + + private readonly CancellationTokenSource _loopCancellationTokenSource = new CancellationTokenSource(); + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private Task _currentAuthenticationStateTask; + + public RevalidatingAuthenticationStateProvider( + IServiceScopeFactory scopeFactory, + SignInManager circuitScopeSignInManager, + ILogger> logger) + { + var initialUser = circuitScopeSignInManager.Context.User; + _currentAuthenticationStateTask = Task.FromResult(new AuthenticationState(initialUser)); + _scopeFactory = scopeFactory; + _logger = logger; + + if (initialUser.Identity.IsAuthenticated) + { + _ = RevalidationLoop(); + } + } + + public override Task GetAuthenticationStateAsync() + => _currentAuthenticationStateTask; + + private async Task RevalidationLoop() + { + var cancellationToken = _loopCancellationTokenSource.Token; + + while (!cancellationToken.IsCancellationRequested) + { + try + { + await Task.Delay(RevalidationInterval, cancellationToken); + } + catch (TaskCanceledException) + { + break; + } + + var isValid = await CheckIfAuthenticationStateIsValidAsync(); + if (!isValid) + { + // Force sign-out. Also stop the revalidation loop, because the user can + // only sign back in by starting a new connection. + var anonymousUser = new ClaimsPrincipal(); + _currentAuthenticationStateTask = Task.FromResult(new AuthenticationState(anonymousUser)); + NotifyAuthenticationStateChanged(_currentAuthenticationStateTask); + _loopCancellationTokenSource.Cancel(); + } + } + } + + private async Task CheckIfAuthenticationStateIsValidAsync() + { + try + { + // Get the sign-in manager from a new scope to ensure it fetches fresh data + using (var scope = _scopeFactory.CreateScope()) + { + var signInManager = scope.ServiceProvider.GetRequiredService>(); + var authenticationState = await _currentAuthenticationStateTask; + var validatedUser = await signInManager.ValidateSecurityStampAsync(authenticationState.User); + return validatedUser != null; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred while revalidating authentication state"); + return false; + } + } + + void IDisposable.Dispose() + => _loopCancellationTokenSource.Cancel(); + } +} diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs index ff517870d6..306308249e 100644 --- a/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs +++ b/src/ProjectTemplates/Web.ProjectTemplates/content/RazorComponentsWeb-CSharp/Startup.cs @@ -36,6 +36,9 @@ using Microsoft.Extensions.Hosting; #if(MultiOrgAuth) using Microsoft.IdentityModel.Tokens; #endif +#if (IndividualLocalAuth) +using RazorComponentsWeb_CSharp.Areas.Identity; +#endif using RazorComponentsWeb_CSharp.Data; namespace RazorComponentsWeb_CSharp @@ -64,6 +67,8 @@ namespace RazorComponentsWeb_CSharp #endif services.AddDefaultIdentity() .AddEntityFrameworkStores(); + + services.AddScoped>(); #elif (OrganizationalAuth) services.AddAuthentication(AzureADDefaults.AuthenticationScheme) .AddAzureAD(options => Configuration.Bind("AzureAd", options)); diff --git a/src/ProjectTemplates/test/template-baselines.json b/src/ProjectTemplates/test/template-baselines.json index 5d610cc2f5..b7466194dd 100644 --- a/src/ProjectTemplates/test/template-baselines.json +++ b/src/ProjectTemplates/test/template-baselines.json @@ -894,6 +894,7 @@ "_Imports.razor", "Areas/Identity/Pages/Account/LogOut.cshtml", "Areas/Identity/Pages/Shared/_LoginPartial.cshtml", + "Areas/Identity/RevalidatingAuthenticationStateProvider.cs", "Data/ApplicationDbContext.cs", "Data/WeatherForecast.cs", "Data/WeatherForecastService.cs",