diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index 331ca6592dac..53afb267b2a6 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -85,6 +85,14 @@ public override Task OnDisconnectedAsync(Exception exception) return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId); } + public override Task OnAuthenticationRefreshedAsync() + { + var circuitHost = _circuitHandleRegistry.GetCircuit(Context.Items, CircuitKey); + circuitHost?.SetCircuitUser(Context.User); + + return Task.CompletedTask; + } + public async ValueTask StartCircuit(string baseUri, string uri, string serializedComponentRecords, string applicationState) { var circuitHost = _circuitHandleRegistry.GetCircuit(Context.Items, CircuitKey); diff --git a/src/Components/Server/test/Circuits/ComponentHubTest.cs b/src/Components/Server/test/Circuits/ComponentHubTest.cs index fb39e0b0cfb1..69bcfe69c827 100644 --- a/src/Components/Server/test/Circuits/ComponentHubTest.cs +++ b/src/Components/Server/test/Circuits/ComponentHubTest.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Text.RegularExpressions; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Server.Circuits; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; @@ -334,11 +335,44 @@ public async Task ResumeCircuitFailsWithUnresolvedCircuitHandlerDependency_Notif mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny()), Times.Once()); } + [Fact] + public async Task OnAuthenticationRefreshedAsyncUpdatesCircuitUser() + { + var authenticationStateProvider = new ServerAuthenticationStateProvider(); + var services = new ServiceCollection() + .AddSingleton(authenticationStateProvider) + .BuildServiceProvider(); + var circuitHost = TestCircuitHost.Create(serviceScope: services.CreateAsyncScope()); + + var handleRegistryMock = new Mock(); + handleRegistryMock.Setup(m => m.GetCircuit(It.IsAny>(), It.IsAny())) + .Returns(circuitHost); + + var refreshedUser = new ClaimsPrincipal(new ClaimsIdentity( + [new Claim(ClaimTypes.Name, "refreshed-user")], + "TestAuthType")); + var (_, hub) = InitializeComponentHub(handleRegistry: handleRegistryMock.Object, user: refreshedUser); + + await hub.OnAuthenticationRefreshedAsync(); + + var authenticationState = await authenticationStateProvider.GetAuthenticationStateAsync(); + Assert.Same(refreshedUser, authenticationState.User); + } + + [Fact] + public async Task OnAuthenticationRefreshedAsyncWithoutCircuitDoesNotThrow() + { + var (_, hub) = InitializeComponentHub(); + + await hub.OnAuthenticationRefreshedAsync(); + } + private static (Mock, ComponentHub) InitializeComponentHub( TestServerComponentDeserializer deserializer = null, ICircuitHandleRegistry handleRegistry = null, ICircuitPersistenceProvider provider = null, - ICircuitFactory circuitFactory = null) + ICircuitFactory circuitFactory = null, + ClaimsPrincipal user = null) { deserializer ??= new TestServerComponentDeserializer(); var ephemeralDataProtectionProvider = new EphemeralDataProtectionProvider(); @@ -384,6 +418,7 @@ private static (Mock, ComponentHub) InitializeComponentHub( feature.Set(httpContextFeature.Object); mockContext.Setup(x => x.Features).Returns(feature); mockContext.Setup(x => x.ConnectionId).Returns("123"); + mockContext.Setup(x => x.User).Returns(user ?? new ClaimsPrincipal()); hub.Context = mockContext.Object; return (mockClientProxy, hub); diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerAuthTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerAuthTest.cs index 9a621f95d887..9bbfdf7174c2 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ServerAuthTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerAuthTest.cs @@ -49,6 +49,34 @@ void AssertState(string username) } } + [Fact] + public void UpdatesAuthenticationStateWhenAuthenticationRefreshed() + { + SignInAs("Someone", "IrrelevantRole"); + var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases, "?captureAuthenticationRefresh"); + Browser.Equal("You're not authorized, Someone", () => + appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text); + + var javascript = (IJavaScriptExecutor)Browser; + var connectionId = Assert.IsType( + javascript.ExecuteScript("return authenticationRefreshConnection.connectionId;")); + + SignInAs("Someone", "TestRole", useSeparateTab: true); + var refreshError = javascript.ExecuteAsyncScript(""" + const callback = arguments[arguments.length - 1]; + authenticationRefreshConnection.refreshAuthentication().then( + () => callback(), + error => callback(String(error))); + """); + + Assert.Null(refreshError); + Browser.Equal("Welcome, Someone!", () => + appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text); + Assert.Equal( + connectionId, + Assert.IsType(javascript.ExecuteScript("return authenticationRefreshConnection.connectionId;"))); + } + private void SignInAs(string usernName, string roles, bool useSeparateTab = false) => Browser.SignInAs(new Uri(_serverFixture.RootUri, "/subdir"), usernName, roles, useSeparateTab); diff --git a/src/Components/test/E2ETest/Tests/AuthTest.cs b/src/Components/test/E2ETest/Tests/AuthTest.cs index e1f0fc12cada..ead4a3be97c3 100644 --- a/src/Components/test/E2ETest/Tests/AuthTest.cs +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -225,9 +225,9 @@ private void AssertExpectedLayoutUsed() Browser.Exists(By.Id("auth-links")); } - protected IWebElement MountAndNavigateToAuthTest(string authLinkText) + protected IWebElement MountAndNavigateToAuthTest(string authLinkText, string queryString = "") { - Navigate(ServerPathBase); + Navigate($"{ServerPathBase}{queryString}"); var appElement = Browser.MountTestComponent(); Browser.Exists(By.Id("auth-links")); appElement.FindElement(By.LinkText(authLinkText)).Click(); diff --git a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs index f92842b82d1b..1b110815a4d4 100644 --- a/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs +++ b/src/Components/test/testassets/Components.TestServer/AuthenticationStartup.cs @@ -61,7 +61,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapControllers(); endpoints.MapRazorPages(); - endpoints.MapBlazorHub() + endpoints.MapBlazorHub(options => options.EnableAuthenticationRefresh = true) .AddEndpointFilter(async (context, next) => { if (context.HttpContext.WebSockets.IsWebSocketRequest) diff --git a/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml b/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml index 9de8ea472840..6de9575c0f98 100644 --- a/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml +++ b/src/Components/test/testassets/Components.TestServer/Pages/_ServerHost.cshtml @@ -54,6 +54,16 @@