Revalidating auth improvement. Fixes #12692 (#12909)

This commit is contained in:
Steve Sanderson 2019-08-07 15:15:26 +01:00 committed by GitHub
parent 15e4b605eb
commit 92869c677f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 472 additions and 109 deletions

View File

@ -35,6 +35,20 @@ namespace Microsoft.AspNetCore.Components.Server
public System.TimeSpan JSInteropDefaultCallTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public int MaxBufferedUnacknowledgedRenderBatches { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public abstract partial class RevalidatingServerAuthenticationStateProvider : Microsoft.AspNetCore.Components.Server.ServerAuthenticationStateProvider, System.IDisposable
{
public RevalidatingServerAuthenticationStateProvider(Microsoft.Extensions.Logging.ILoggerFactory loggerFactory) { }
protected abstract System.TimeSpan RevalidationInterval { get; }
protected virtual void Dispose(bool disposing) { }
void System.IDisposable.Dispose() { }
protected abstract System.Threading.Tasks.Task<bool> ValidateAuthenticationStateAsync(Microsoft.AspNetCore.Components.AuthenticationState authenticationState, System.Threading.CancellationToken cancellationToken);
}
public partial class ServerAuthenticationStateProvider : Microsoft.AspNetCore.Components.AuthenticationStateProvider, Microsoft.AspNetCore.Components.IHostEnvironmentAuthenticationStateProvider
{
public ServerAuthenticationStateProvider() { }
public override System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> GetAuthenticationStateAsync() { throw null; }
public void SetAuthenticationState(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> authenticationStateTask) { }
}
}
namespace Microsoft.AspNetCore.Components.Server.Circuits
{

View File

@ -58,13 +58,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
jsRuntime.Initialize(client);
componentContext.Initialize(client);
var authenticationStateProvider = scope.ServiceProvider.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
if (authenticationStateProvider != null)
{
var authenticationState = new AuthenticationState(httpContext.User); // TODO: Get this from the hub connection context instead
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
}
var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
if (client.Connected)

View File

@ -0,0 +1,119 @@
// 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.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Server
{
/// <summary>
/// A base class for <see cref="AuthenticationStateProvider"/> services that receive an
/// authentication state from the host environment, and revalidate it at regular intervals.
/// </summary>
public abstract class RevalidatingServerAuthenticationStateProvider
: ServerAuthenticationStateProvider, IDisposable
{
private readonly ILogger _logger;
private CancellationTokenSource _loopCancellationTokenSource = new CancellationTokenSource();
/// <summary>
/// Constructs an instance of <see cref="RevalidatingServerAuthenticationStateProvider"/>.
/// </summary>
/// <param name="loggerFactory">A logger factory.</param>
public RevalidatingServerAuthenticationStateProvider(ILoggerFactory loggerFactory)
{
if (loggerFactory is null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
_logger = loggerFactory.CreateLogger<RevalidatingServerAuthenticationStateProvider>();
// 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);
};
}
/// <summary>
/// Gets the interval between revalidation attempts.
/// </summary>
protected abstract TimeSpan RevalidationInterval { get; }
/// <summary>
/// Determines whether the authentication state is still valid.
/// </summary>
/// <param name="authenticationState">The current <see cref="AuthenticationState"/>.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to observe while performing the operation.</param>
/// <returns>A <see cref="Task"/> that resolves as true if the <paramref name="authenticationState"/> is still valid, or false if it is not.</returns>
protected abstract Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken);
private async Task RevalidationLoop(Task<AuthenticationState> 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);
}
/// <inheritdoc />
protected virtual void Dispose(bool disposing)
{
}
}
}

View File

@ -4,19 +4,21 @@
using System;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Server.Circuits
namespace Microsoft.AspNetCore.Components.Server
{
/// <summary>
/// An <see cref="AuthenticationStateProvider"/> intended for use in server-side Blazor.
/// </summary>
internal class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
public class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
{
private Task<AuthenticationState> _authenticationStateTask;
/// <inheritdoc />
public override Task<AuthenticationState> GetAuthenticationStateAsync()
=> _authenticationStateTask
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}.");
/// <inheritdoc />
public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
{
_authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));

View File

@ -0,0 +1,256 @@
// 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.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Components
{
public class RevalidatingServerAuthenticationStateProviderTest
{
[Fact]
public void AcceptsAndReturnsAuthStateFromHost()
{
// Arrange
using var provider = new TestRevalidatingServerAuthenticationStateProvider(TimeSpan.MaxValue);
// Act/Assert: Host can supply a value
var hostAuthStateTask = (new TaskCompletionSource<AuthenticationState>()).Task;
provider.SetAuthenticationState(hostAuthStateTask);
Assert.Same(hostAuthStateTask, provider.GetAuthenticationStateAsync());
// Act/Assert: Host can supply a changed value
var hostAuthStateTask2 = (new TaskCompletionSource<AuthenticationState>()).Task;
provider.SetAuthenticationState(hostAuthStateTask2);
Assert.Same(hostAuthStateTask2, provider.GetAuthenticationStateAsync());
}
[Fact]
public async Task IfValidateAuthenticationStateAsyncReturnsTrue_ContinuesRevalidating()
{
// Arrange
using var provider = new TestRevalidatingServerAuthenticationStateProvider(
TimeSpan.FromMilliseconds(50));
provider.SetAuthenticationState(CreateAuthenticationStateTask("test user"));
provider.NextValidationResult = Task.FromResult(true);
var didNotifyAuthenticationStateChanged = false;
provider.AuthenticationStateChanged += _ => { didNotifyAuthenticationStateChanged = true; };
// Act
for (var i = 0; i < 10; i++)
{
await provider.NextValidateAuthenticationStateAsyncCall;
}
// Assert
Assert.Equal(10, provider.RevalidationCallLog.Count);
Assert.False(didNotifyAuthenticationStateChanged);
Assert.Equal("test user", (await provider.GetAuthenticationStateAsync()).User.Identity.Name);
}
[Fact]
public async Task IfValidateAuthenticationStateAsyncReturnsFalse_ForcesSignOut()
{
// Arrange
using var provider = new TestRevalidatingServerAuthenticationStateProvider(
TimeSpan.FromMilliseconds(50));
provider.SetAuthenticationState(CreateAuthenticationStateTask("test user"));
provider.NextValidationResult = Task.FromResult(false);
var newAuthStateNotificationTcs = new TaskCompletionSource<Task<AuthenticationState>>();
provider.AuthenticationStateChanged += newStateTask => newAuthStateNotificationTcs.SetResult(newStateTask);
// Act
var newAuthStateTask = await newAuthStateNotificationTcs.Task;
var newAuthState = await newAuthStateTask;
// Assert
Assert.False(newAuthState.User.Identity.IsAuthenticated);
// Assert: no longer revalidates
await Task.Delay(200);
Assert.Single(provider.RevalidationCallLog);
}
[Fact]
public async Task IfValidateAuthenticationStateAsyncThrows_ForcesSignOut()
{
// Arrange
using var provider = new TestRevalidatingServerAuthenticationStateProvider(
TimeSpan.FromMilliseconds(50));
provider.SetAuthenticationState(CreateAuthenticationStateTask("test user"));
provider.NextValidationResult = Task.FromException<bool>(new InvalidTimeZoneException());
var newAuthStateNotificationTcs = new TaskCompletionSource<Task<AuthenticationState>>();
provider.AuthenticationStateChanged += newStateTask => newAuthStateNotificationTcs.SetResult(newStateTask);
// Act
var newAuthStateTask = await newAuthStateNotificationTcs.Task;
var newAuthState = await newAuthStateTask;
// Assert
Assert.False(newAuthState.User.Identity.IsAuthenticated);
// Assert: no longer revalidates
await Task.Delay(200);
Assert.Single(provider.RevalidationCallLog);
}
[Fact]
public async Task IfHostSuppliesNewAuthenticationState_RestartsRevalidationLoop()
{
// Arrange
using var provider = new TestRevalidatingServerAuthenticationStateProvider(
TimeSpan.FromMilliseconds(50));
provider.SetAuthenticationState(CreateAuthenticationStateTask("test user"));
provider.NextValidationResult = Task.FromResult(true);
await provider.NextValidateAuthenticationStateAsyncCall;
Assert.Collection(provider.RevalidationCallLog,
call => Assert.Equal("test user", call.AuthenticationState.User.Identity.Name));
// Act/Assert 1: Can become signed out
// Doesn't revalidate unauthenticated states
provider.SetAuthenticationState(CreateAuthenticationStateTask(null));
await Task.Delay(200);
Assert.Empty(provider.RevalidationCallLog.Skip(1));
// Act/Assert 2: Can become a different user; resumes revalidation
provider.SetAuthenticationState(CreateAuthenticationStateTask("different user"));
await provider.NextValidateAuthenticationStateAsyncCall;
Assert.Collection(provider.RevalidationCallLog.Skip(1),
call => Assert.Equal("different user", call.AuthenticationState.User.Identity.Name));
}
[Fact]
public async Task StopsRevalidatingAfterDisposal()
{
// Arrange
using var provider = new TestRevalidatingServerAuthenticationStateProvider(
TimeSpan.FromMilliseconds(50));
provider.SetAuthenticationState(CreateAuthenticationStateTask("test user"));
provider.NextValidationResult = Task.FromResult(true);
// Act
((IDisposable)provider).Dispose();
await Task.Delay(200);
// Assert
Assert.Empty(provider.RevalidationCallLog);
}
[Fact]
public async Task SuppliesCancellationTokenThatSignalsWhenRevalidationLoopIsBeingDiscarded()
{
// Arrange
var validationTcs = new TaskCompletionSource<bool>();
var authenticationStateChangedCount = 0;
using var provider = new TestRevalidatingServerAuthenticationStateProvider(
TimeSpan.FromMilliseconds(50));
provider.NextValidationResult = validationTcs.Task;
provider.SetAuthenticationState(CreateAuthenticationStateTask("test user"));
provider.AuthenticationStateChanged += _ => { authenticationStateChangedCount++; };
// Act/Assert 1: token isn't cancelled initially
await provider.NextValidateAuthenticationStateAsyncCall;
var firstRevalidationCall = provider.RevalidationCallLog.Single();
Assert.False(firstRevalidationCall.CancellationToken.IsCancellationRequested);
Assert.Equal(0, authenticationStateChangedCount);
// Have the task throw a TCE to show this doesn't get treated as a failure
firstRevalidationCall.CancellationToken.Register(() => validationTcs.TrySetCanceled(firstRevalidationCall.CancellationToken));
// Act/Assert 2: token is cancelled when the loop is superseded
provider.NextValidationResult = Task.FromResult(true);
provider.SetAuthenticationState(CreateAuthenticationStateTask("different user"));
Assert.True(firstRevalidationCall.CancellationToken.IsCancellationRequested);
// Since we asked for that operation to be cancelled, we don't treat it as a failure and
// don't force a logout
Assert.Equal(1, authenticationStateChangedCount);
Assert.Equal("different user", (await provider.GetAuthenticationStateAsync()).User.Identity.Name);
// Subsequent revalidation can complete successfully
await provider.NextValidateAuthenticationStateAsyncCall;
Assert.Collection(provider.RevalidationCallLog.Skip(1),
call => Assert.Equal("different user", call.AuthenticationState.User.Identity.Name));
}
[Fact]
public async Task IfValidateAuthenticationStateAsyncReturnsUnrelatedCancelledTask_TreatAsFailure()
{
// Arrange
var validationTcs = new TaskCompletionSource<bool>();
var authenticationStateChangedCount = 0;
using var provider = new TestRevalidatingServerAuthenticationStateProvider(
TimeSpan.FromMilliseconds(50));
provider.NextValidationResult = validationTcs.Task;
provider.SetAuthenticationState(CreateAuthenticationStateTask("test user"));
provider.AuthenticationStateChanged += _ => { authenticationStateChangedCount++; };
// Be waiting for the first ValidateAuthenticationStateAsync to complete
await provider.NextValidateAuthenticationStateAsyncCall;
var firstRevalidationCall = provider.RevalidationCallLog.Single();
Assert.Equal(0, authenticationStateChangedCount);
// Act: ValidateAuthenticationStateAsync returns cancelled task, but the cancellation
// is unrelated to the CT we supplied
validationTcs.TrySetCanceled(new CancellationTokenSource().Token);
// Assert: Since we didn't ask for that operation to be cancelled, this is treated as
// a failure to validate, so we force a logout
Assert.Equal(1, authenticationStateChangedCount);
var newAuthState = await provider.GetAuthenticationStateAsync();
Assert.False(newAuthState.User.Identity.IsAuthenticated);
Assert.Null(newAuthState.User.Identity.Name);
}
static Task<AuthenticationState> CreateAuthenticationStateTask(string username)
{
var identity = !string.IsNullOrEmpty(username)
? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username) }, "testauth")
: new ClaimsIdentity();
var authenticationState = new AuthenticationState(new ClaimsPrincipal(identity));
return Task.FromResult(authenticationState);
}
class TestRevalidatingServerAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider
{
private readonly TimeSpan _revalidationInterval;
private TaskCompletionSource<object> _nextValidateAuthenticationStateAsyncCallSource
= new TaskCompletionSource<object>();
public TestRevalidatingServerAuthenticationStateProvider(TimeSpan revalidationInterval)
: base(NullLoggerFactory.Instance)
{
_revalidationInterval = revalidationInterval;
}
public Task<bool> NextValidationResult { get; set; }
public Task NextValidateAuthenticationStateAsyncCall
=> _nextValidateAuthenticationStateAsyncCallSource.Task;
public List<(AuthenticationState AuthenticationState, CancellationToken CancellationToken)> RevalidationCallLog { get; }
= new List<(AuthenticationState, CancellationToken)>();
protected override TimeSpan RevalidationInterval => _revalidationInterval;
protected override Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken)
{
RevalidationCallLog.Add((authenticationState, cancellationToken));
var result = NextValidationResult;
var prevCts = _nextValidateAuthenticationStateAsyncCallSource;
_nextValidateAuthenticationStateAsyncCallSource = new TaskCompletionSource<object>();
prevCts.SetResult(true);
return result;
}
}
}
}

View File

@ -10,6 +10,8 @@ using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.Testing.xunit;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
@ -17,6 +19,7 @@ using Xunit;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
[Flaky("https://github.com/aspnet/AspNetCore/issues/12940", FlakyOn.All)]
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>
{
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500);

View File

@ -1,98 +0,0 @@
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 BlazorServerWeb_CSharp.Areas.Identity
{
/// <summary>
/// An <see cref="AuthenticationStateProvider"/> 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.
/// </summary>
/// <typeparam name="TUser">The type encapsulating a user.</typeparam>
public class RevalidatingAuthenticationStateProvider<TUser>
: 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<AuthenticationState> _currentAuthenticationStateTask;
public RevalidatingAuthenticationStateProvider(
IServiceScopeFactory scopeFactory,
SignInManager<TUser> circuitScopeSignInManager,
ILogger<RevalidatingAuthenticationStateProvider<TUser>> logger)
{
var initialUser = circuitScopeSignInManager.Context.User;
_currentAuthenticationStateTask = Task.FromResult(new AuthenticationState(initialUser));
_scopeFactory = scopeFactory;
_logger = logger;
if (initialUser.Identity.IsAuthenticated)
{
_ = RevalidationLoop();
}
}
public override Task<AuthenticationState> 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(new ClaimsIdentity());
_currentAuthenticationStateTask = Task.FromResult(new AuthenticationState(anonymousUser));
NotifyAuthenticationStateChanged(_currentAuthenticationStateTask);
_loopCancellationTokenSource.Cancel();
}
}
}
private async Task<bool> 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<SignInManager<TUser>>();
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();
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BlazorServerWeb_CSharp.Areas.Identity
{
public class RevalidatingIdentityAuthenticationStateProvider<TUser>
: RevalidatingServerAuthenticationStateProvider where TUser : class
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly IdentityOptions _options;
public RevalidatingIdentityAuthenticationStateProvider(
ILoggerFactory loggerFactory,
IServiceScopeFactory scopeFactory,
IOptions<IdentityOptions> optionsAccessor)
: base(loggerFactory)
{
_scopeFactory = scopeFactory;
_options = optionsAccessor.Value;
}
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
var scope = _scopeFactory.CreateScope();
try
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<TUser>>();
return await ValidateSecurityStampAsync(userManager, authenticationState.User);
}
finally
{
if (scope is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
private async Task<bool> ValidateSecurityStampAsync(UserManager<TUser> userManager, ClaimsPrincipal principal)
{
var user = await userManager.GetUserAsync(principal);
if (user == 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;
}
}
}
}

View File

@ -127,7 +127,7 @@ namespace BlazorServerWeb_CSharp
services.AddRazorPages();
services.AddServerSideBlazor();
#if (IndividualLocalAuth)
services.AddScoped<AuthenticationStateProvider, RevalidatingAuthenticationStateProvider<IdentityUser>>();
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
#endif
services.AddSingleton<WeatherForecastService>();
}

View File

@ -905,7 +905,7 @@
"_Imports.razor",
"Areas/Identity/Pages/Account/LogOut.cshtml",
"Areas/Identity/Pages/Shared/_LoginPartial.cshtml",
"Areas/Identity/RevalidatingAuthenticationStateProvider.cs",
"Areas/Identity/RevalidatingIdentityAuthenticationStateProvider.cs",
"Data/ApplicationDbContext.cs",
"Data/WeatherForecast.cs",
"Data/WeatherForecastService.cs",