parent
15e4b605eb
commit
92869c677f
|
|
@ -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 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 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
|
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -58,13 +58,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
jsRuntime.Initialize(client);
|
jsRuntime.Initialize(client);
|
||||||
componentContext.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 navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>();
|
||||||
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
|
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
|
||||||
if (client.Connected)
|
if (client.Connected)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,19 +4,21 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
namespace Microsoft.AspNetCore.Components.Server
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An <see cref="AuthenticationStateProvider"/> intended for use in server-side Blazor.
|
/// An <see cref="AuthenticationStateProvider"/> intended for use in server-side Blazor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
|
public class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
|
||||||
{
|
{
|
||||||
private Task<AuthenticationState> _authenticationStateTask;
|
private Task<AuthenticationState> _authenticationStateTask;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
public override Task<AuthenticationState> GetAuthenticationStateAsync()
|
||||||
=> _authenticationStateTask
|
=> _authenticationStateTask
|
||||||
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}.");
|
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}.");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
|
public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
|
||||||
{
|
{
|
||||||
_authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));
|
_authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,6 +10,8 @@ using Ignitor;
|
||||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
|
using Microsoft.AspNetCore.Testing;
|
||||||
|
using Microsoft.AspNetCore.Testing.xunit;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Testing;
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
|
@ -17,6 +19,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
{
|
{
|
||||||
|
[Flaky("https://github.com/aspnet/AspNetCore/issues/12940", FlakyOn.All)]
|
||||||
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>
|
public class InteropReliabilityTests : IClassFixture<AspNetSiteServerFixture>
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500);
|
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromMilliseconds(500);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -127,7 +127,7 @@ namespace BlazorServerWeb_CSharp
|
||||||
services.AddRazorPages();
|
services.AddRazorPages();
|
||||||
services.AddServerSideBlazor();
|
services.AddServerSideBlazor();
|
||||||
#if (IndividualLocalAuth)
|
#if (IndividualLocalAuth)
|
||||||
services.AddScoped<AuthenticationStateProvider, RevalidatingAuthenticationStateProvider<IdentityUser>>();
|
services.AddScoped<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<IdentityUser>>();
|
||||||
#endif
|
#endif
|
||||||
services.AddSingleton<WeatherForecastService>();
|
services.AddSingleton<WeatherForecastService>();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -905,7 +905,7 @@
|
||||||
"_Imports.razor",
|
"_Imports.razor",
|
||||||
"Areas/Identity/Pages/Account/LogOut.cshtml",
|
"Areas/Identity/Pages/Account/LogOut.cshtml",
|
||||||
"Areas/Identity/Pages/Shared/_LoginPartial.cshtml",
|
"Areas/Identity/Pages/Shared/_LoginPartial.cshtml",
|
||||||
"Areas/Identity/RevalidatingAuthenticationStateProvider.cs",
|
"Areas/Identity/RevalidatingIdentityAuthenticationStateProvider.cs",
|
||||||
"Data/ApplicationDbContext.cs",
|
"Data/ApplicationDbContext.cs",
|
||||||
"Data/WeatherForecast.cs",
|
"Data/WeatherForecast.cs",
|
||||||
"Data/WeatherForecastService.cs",
|
"Data/WeatherForecastService.cs",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue