Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Components/Server/src/ComponentHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> StartCircuit(string baseUri, string uri, string serializedComponentRecords, string applicationState)
{
var circuitHost = _circuitHandleRegistry.GetCircuit(Context.Items, CircuitKey);
Expand Down
37 changes: 36 additions & 1 deletion src/Components/Server/test/Circuits/ComponentHubTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -334,11 +335,44 @@ public async Task ResumeCircuitFailsWithUnresolvedCircuitHandlerDependency_Notif
mockClientProxy.Verify(m => m.SendCoreAsync("JS.Error", new[] { errorMessage }, It.IsAny<CancellationToken>()), Times.Once());
}

[Fact]
public async Task OnAuthenticationRefreshedAsyncUpdatesCircuitUser()
{
var authenticationStateProvider = new ServerAuthenticationStateProvider();
var services = new ServiceCollection()
.AddSingleton<AuthenticationStateProvider>(authenticationStateProvider)
.BuildServiceProvider();
var circuitHost = TestCircuitHost.Create(serviceScope: services.CreateAsyncScope());

var handleRegistryMock = new Mock<ICircuitHandleRegistry>();
handleRegistryMock.Setup(m => m.GetCircuit(It.IsAny<IDictionary<object, object>>(), It.IsAny<object>()))
.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<ISingleClientProxy>, 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();
Expand Down Expand Up @@ -384,6 +418,7 @@ private static (Mock<ISingleClientProxy>, 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);
hub.Context = mockContext.Object;
Comment thread
kotlarmilos marked this conversation as resolved.

return (mockClientProxy, hub);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,29 @@ 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;
Assert.Equal(1L, javascript.ExecuteScript("return authenticationRefreshTest.negotiateCount;"));

SignInAs("Someone", "TestRole", useSeparateTab: true);
var refreshStatus = javascript.ExecuteAsyncScript("""
const callback = arguments[arguments.length - 1];
authenticationRefreshTest.refresh().then(callback);
""");
Comment thread
kotlarmilos marked this conversation as resolved.
Outdated

Assert.Equal(200L, refreshStatus);
Browser.Equal("Welcome, Someone!", () =>
appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text);
Assert.Equal(1L, javascript.ExecuteScript("return authenticationRefreshTest.negotiateCount;"));
}

private void SignInAs(string usernName, string roles, bool useSeparateTab = false) =>
Browser.SignInAs(new Uri(_serverFixture.RootUri, "/subdir"), usernName, roles, useSeparateTab);

Expand Down
4 changes: 2 additions & 2 deletions src/Components/test/E2ETest/Tests/AuthTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<BasicTestApp.AuthTest.AuthRouter>();
Browser.Exists(By.Id("auth-links"));
appElement.FindElement(By.LinkText(authLinkText)).Click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@

<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
if (new URLSearchParams(location.search).has('captureAuthenticationRefresh')) {
const originalFetch = window.fetch;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really hacky, I don't know if you want this in the code base. Ideally, you'd either get the HubConnection and call the refresh method, or have a short lived token that causes the automatic refresh to occur (yes I know that would cause the test to not be instant anymore).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I removed the interception and now wrap the builder build call inside configureSignalR to retrieve the HubConnection. The test now calls refreshAuthentication directly, verifies the authorization state updates, and confirms the connection ID didn't change.

window.authenticationRefreshTest = {
negotiateCount: 0,
async refresh() {
const url = new URL('_blazor/refresh', document.baseURI);
url.searchParams.set('id', this.connectionToken);
return (await originalFetch(url, { method: 'POST' })).status;
},
};
window.fetch = async (input, init) => {
const response = await originalFetch(input, init);
if (`${input}`.includes('/negotiate')) {
authenticationRefreshTest.negotiateCount++;
authenticationRefreshTest.connectionToken = (await response.clone().json()).connectionToken;
}
return response;
};
}

Blazor.start({
reconnectionOptions: {
// It's easier to test the reconnection logic if we wait a bit
Expand Down
Loading