Skip to content

Commit 8b7fcf1

Browse files
Update user on reconnect. Fixes #12051 (#12421)
1 parent 9b6f10d commit 8b7fcf1

File tree

12 files changed

+154
-47
lines changed

12 files changed

+154
-47
lines changed

src/Components/Server/src/Circuits/CircuitFactory.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System.Security.Claims;
45
using Microsoft.AspNetCore.Http;
56

67
namespace Microsoft.AspNetCore.Components.Server.Circuits
@@ -11,6 +12,7 @@ public abstract CircuitHost CreateCircuitHost(
1112
HttpContext httpContext,
1213
CircuitClientProxy client,
1314
string uriAbsolute,
14-
string baseUriAbsolute);
15+
string baseUriAbsolute,
16+
ClaimsPrincipal user);
1517
}
1618
}

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Security.Claims;
67
using System.Text.Json;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -109,6 +110,16 @@ public Task<ComponentRenderedText> PrerenderComponentAsync(Type componentType, P
109110
});
110111
}
111112

113+
public void SetCircuitUser(ClaimsPrincipal user)
114+
{
115+
var authenticationStateProvider = Services.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
116+
if (authenticationStateProvider != null)
117+
{
118+
var authenticationState = new AuthenticationState(user);
119+
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
120+
}
121+
}
122+
112123
internal void InitializeCircuitAfterPrerender(UnhandledExceptionEventHandler unhandledException)
113124
{
114125
if (!_initialized)

src/Components/Server/src/Circuits/CircuitPrerenderer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ private CircuitHost GetOrCreateCircuitHost(HttpContext context, PrerenderingCanc
132132
context,
133133
client: new CircuitClientProxy(), // This creates an "offline" client.
134134
GetFullUri(context.Request),
135-
GetFullBaseUri(context.Request));
135+
GetFullBaseUri(context.Request),
136+
context.User);
136137

137138
result.UnhandledException += CircuitHost_UnhandledException;
138139
context.Response.OnCompleted(() =>

src/Components/Server/src/Circuits/DefaultCircuitFactory.cs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Security.Claims;
78
using System.Text.Encodings.Web;
89
using Microsoft.AspNetCore.Components.Web;
910
using Microsoft.AspNetCore.Components.Web.Rendering;
10-
using Microsoft.AspNetCore.Components.Rendering;
1111
using Microsoft.AspNetCore.Components.Routing;
1212
using Microsoft.AspNetCore.Http;
1313
using Microsoft.AspNetCore.Http.Features;
1414
using Microsoft.Extensions.DependencyInjection;
1515
using Microsoft.Extensions.Logging;
1616
using Microsoft.JSInterop;
17-
using System.Threading.Tasks;
1817

1918
namespace Microsoft.AspNetCore.Components.Server.Circuits
2019
{
@@ -40,7 +39,8 @@ public override CircuitHost CreateCircuitHost(
4039
HttpContext httpContext,
4140
CircuitClientProxy client,
4241
string uriAbsolute,
43-
string baseUriAbsolute)
42+
string baseUriAbsolute,
43+
ClaimsPrincipal user)
4444
{
4545
var components = ResolveComponentMetadata(httpContext, client);
4646

@@ -51,13 +51,6 @@ public override CircuitHost CreateCircuitHost(
5151
jsRuntime.Initialize(client);
5252
componentContext.Initialize(client);
5353

54-
var authenticationStateProvider = scope.ServiceProvider.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
55-
if (authenticationStateProvider != null)
56-
{
57-
var authenticationState = new AuthenticationState(httpContext.User); // TODO: Get this from the hub connection context instead
58-
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
59-
}
60-
6154
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
6255
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
6356
if (client.Connected)
@@ -102,6 +95,7 @@ public override CircuitHost CreateCircuitHost(
10295

10396
// Initialize per - circuit data that services need
10497
(circuitHost.Services.GetRequiredService<ICircuitAccessor>() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit;
98+
circuitHost.SetCircuitUser(user);
10599

106100
return circuitHost;
107101
}

src/Components/Server/src/ComponentHub.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ public string StartCircuit(string uriAbsolute, string baseUriAbsolute)
9797
Context.GetHttpContext(),
9898
circuitClient,
9999
uriAbsolute,
100-
baseUriAbsolute);
100+
baseUriAbsolute,
101+
Context.User);
101102

102103
circuitHost.UnhandledException += CircuitHost_UnhandledException;
103104

@@ -125,6 +126,7 @@ public async Task<bool> ConnectCircuit(string circuitId)
125126
CircuitHost = circuitHost;
126127

127128
circuitHost.InitializeCircuitAfterPrerender(CircuitHost_UnhandledException);
129+
circuitHost.SetCircuitUser(Context.User);
128130
circuitHost.SendPendingBatches();
129131
return true;
130132
}

src/Components/Server/test/Circuits/CircuitPrerendererTest.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.IO;
6+
using System.Security.Claims;
67
using System.Text.Json;
78
using System.Text.RegularExpressions;
89
using System.Threading.Tasks;
@@ -190,7 +191,7 @@ public TestCircuitFactory(Func<string> circuitIdFactory = null)
190191
_circuitIdFactory = circuitIdFactory ?? (() => Guid.NewGuid().ToString());
191192
}
192193

193-
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute)
194+
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute, ClaimsPrincipal user)
194195
{
195196
var serviceCollection = new ServiceCollection();
196197
serviceCollection.AddScoped<IUriHelper>(_ =>
@@ -209,7 +210,7 @@ class MockServiceScopeCircuitFactory : CircuitFactory
209210
public Mock<IServiceScope> MockServiceScope { get; }
210211
= new Mock<IServiceScope>();
211212

212-
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute)
213+
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute, ClaimsPrincipal user)
213214
{
214215
return TestCircuitHost.Create(Guid.NewGuid().ToString(), MockServiceScope.Object);
215216
}

src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using BasicTestApp;
5-
using Microsoft.AspNetCore.Components;
65
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
76
using Microsoft.AspNetCore.E2ETesting;
87
using OpenQA.Selenium;
98
using OpenQA.Selenium.Support.UI;
109
using System;
10+
using System.Linq;
1111
using Xunit.Abstractions;
1212

1313
namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
@@ -49,5 +49,32 @@ protected IWebElement WaitUntilExists(By findBy, int timeoutSeconds = 10)
4949
.Until(driver => (result = driver.FindElement(findBy)) != null);
5050
return result;
5151
}
52+
53+
protected void SignInAs(string usernameOrNull, string rolesOrNull, bool useSeparateTab = false)
54+
{
55+
const string authenticationPageUrl = "/Authentication";
56+
var baseRelativeUri = usernameOrNull == null
57+
? $"{authenticationPageUrl}?signout=true"
58+
: $"{authenticationPageUrl}?username={usernameOrNull}&roles={rolesOrNull}";
59+
60+
if (useSeparateTab)
61+
{
62+
// Some tests need to change the authentication state without discarding the
63+
// original page, but this adds several seconds of delay
64+
var javascript = (IJavaScriptExecutor)Browser;
65+
var originalWindow = Browser.CurrentWindowHandle;
66+
javascript.ExecuteScript("window.open()");
67+
Browser.SwitchTo().Window(Browser.WindowHandles.Last());
68+
Navigate(baseRelativeUri);
69+
WaitUntilExists(By.CssSelector("h1#authentication"));
70+
javascript.ExecuteScript("window.close()");
71+
Browser.SwitchTo().Window(originalWindow);
72+
}
73+
else
74+
{
75+
Navigate(baseRelativeUri);
76+
WaitUntilExists(By.CssSelector("h1#authentication"));
77+
}
78+
}
5279
}
5380
}

src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Net;
66
using System.Net.Http;
77
using System.Threading.Tasks;
8+
using BasicTestApp;
89
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
910
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
1011
using Microsoft.AspNetCore.E2ETesting;
@@ -14,16 +15,15 @@
1415

1516
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
1617
{
17-
public class PrerenderingTest : ServerTestBase<AspNetSiteServerFixture>
18+
[Collection("auth")] // Because auth uses cookies, this can't run in parallel with other auth tests
19+
public class PrerenderingTest : BasicTestAppTestBase
1820
{
1921
public PrerenderingTest(
2022
BrowserFixture browserFixture,
21-
AspNetSiteServerFixture serverFixture,
23+
ToggleExecutionModeServerFixture<Program> serverFixture,
2224
ITestOutputHelper output)
23-
: base(browserFixture, serverFixture, output)
25+
: base(browserFixture, serverFixture.WithServerExecution(), output)
2426
{
25-
_serverFixture.Environment = AspNetEnvironment.Development;
26-
_serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
2727
}
2828

2929
[Fact]
@@ -97,6 +97,24 @@ public async Task CanRedirectDuringPrerendering(string destinationParam, string
9797
Assert.Equal(expectedUri, response.Headers.Location);
9898
}
9999

100+
[Theory]
101+
[InlineData(null, null)]
102+
[InlineData(null, "Bert")]
103+
[InlineData("Bert", null)]
104+
[InlineData("Bert", "Treb")]
105+
public void CanAccessAuthenticationStateDuringStaticPrerendering(string initialUsername, string interactiveUsername)
106+
{
107+
// See that the authentication state is usable during the initial prerendering
108+
SignInAs(initialUsername, null);
109+
Navigate("/prerendered/prerendered-transition");
110+
Browser.Equal($"Hello, {initialUsername ?? "anonymous"}!", () => Browser.FindElement(By.TagName("h1")).Text);
111+
112+
// See that during connection, we update to whatever the latest authentication state now is
113+
SignInAs(interactiveUsername, null, useSeparateTab: true);
114+
BeginInteractivity();
115+
Browser.Equal($"Hello, {interactiveUsername ?? "anonymous"}!", () => Browser.FindElement(By.TagName("h1")).Text);
116+
}
117+
100118
private void BeginInteractivity()
101119
{
102120
Browser.FindElement(By.Id("load-boot-script")).Click();
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using BasicTestApp;
6+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
7+
using Microsoft.AspNetCore.Components.E2ETest.Tests;
8+
using Microsoft.AspNetCore.E2ETesting;
9+
using OpenQA.Selenium;
10+
using OpenQA.Selenium.Support.UI;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
14+
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
15+
{
16+
public class ServerAuthTest : AuthTest
17+
{
18+
public ServerAuthTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
19+
: base(browserFixture, serverFixture.WithServerExecution(), output)
20+
{
21+
}
22+
23+
[Theory]
24+
[InlineData(null, null)]
25+
[InlineData(null, "Someone")]
26+
[InlineData("Someone", null)]
27+
[InlineData("Someone", "Someone")]
28+
public void UpdatesAuthenticationStateWhenReconnecting(
29+
string usernameBefore, string usernameAfter)
30+
{
31+
// Establish state before disconnection
32+
SignInAs(usernameBefore, usernameBefore == null ? null : "TestRole");
33+
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
34+
AssertState(usernameBefore);
35+
36+
// Change authentication state and force reconnection
37+
SignInAs(usernameAfter, usernameAfter == null ? null : "TestRole", useSeparateTab: true);
38+
PerformReconnection();
39+
AssertState(usernameAfter);
40+
41+
void AssertState(string username)
42+
{
43+
if (username == null)
44+
{
45+
Browser.Equal("You're not authorized, anonymous", () =>
46+
appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text);
47+
}
48+
else
49+
{
50+
Browser.Equal($"Welcome, {username}!", () =>
51+
appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text);
52+
}
53+
}
54+
}
55+
56+
private void PerformReconnection()
57+
{
58+
((IJavaScriptExecutor)Browser).ExecuteScript("Blazor._internal.forceCloseConnection()");
59+
60+
// Wait until the reconnection dialog has been shown but is now hidden
61+
new WebDriverWait(Browser, TimeSpan.FromSeconds(10))
62+
.Until(driver => driver.FindElement(By.Id("components-reconnect-modal"))?.GetCssValue("display") == "none");
63+
}
64+
}
65+
}

src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,4 @@ public ServerKeyTest(BrowserFixture browserFixture, ToggleExecutionModeServerFix
8383
{
8484
}
8585
}
86-
87-
public class ServerAuthTest : AuthTest
88-
{
89-
public ServerAuthTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
90-
: base(browserFixture, serverFixture.WithServerExecution(), output)
91-
{
92-
}
93-
}
9486
}

src/Components/test/E2ETest/Tests/AuthTest.cs

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111

1212
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
1313
{
14+
[Collection("auth")] // Because auth uses cookies, this can't run in parallel with other auth tests
1415
public class AuthTest : BasicTestAppTestBase
1516
{
1617
// These strings correspond to the links in BasicTestApp\AuthTest\Links.razor
17-
const string CascadingAuthenticationStateLink = "Cascading authentication state";
18-
const string AuthorizeViewCases = "AuthorizeView cases";
19-
const string PageAllowingAnonymous = "Page allowing anonymous";
20-
const string PageRequiringAuthorization = "Page requiring any authentication";
21-
const string PageRequiringPolicy = "Page requiring policy";
22-
const string PageRequiringRole = "Page requiring role";
18+
protected const string CascadingAuthenticationStateLink = "Cascading authentication state";
19+
protected const string AuthorizeViewCases = "AuthorizeView cases";
20+
protected const string PageAllowingAnonymous = "Page allowing anonymous";
21+
protected const string PageRequiringAuthorization = "Page requiring any authentication";
22+
protected const string PageRequiringPolicy = "Page requiring policy";
23+
protected const string PageRequiringRole = "Page requiring role";
2324

2425
public AuthTest(
2526
BrowserFixture browserFixture,
@@ -184,23 +185,13 @@ public void Router_RequireRole_NotAuthorized()
184185
appElement.FindElement(By.CssSelector("#auth-failure")).Text);
185186
}
186187

187-
IWebElement MountAndNavigateToAuthTest(string authLinkText)
188+
protected IWebElement MountAndNavigateToAuthTest(string authLinkText)
188189
{
189190
Navigate(ServerPathBase);
190191
var appElement = MountTestComponent<BasicTestApp.AuthTest.AuthRouter>();
191192
WaitUntilExists(By.Id("auth-links"));
192193
appElement.FindElement(By.LinkText(authLinkText)).Click();
193194
return appElement;
194195
}
195-
196-
void SignInAs(string usernameOrNull, string rolesOrNull)
197-
{
198-
const string authenticationPageUrl = "/Authentication";
199-
var baseRelativeUri = usernameOrNull == null
200-
? $"{authenticationPageUrl}?signout=true"
201-
: $"{authenticationPageUrl}?username={usernameOrNull}&roles={rolesOrNull}";
202-
Navigate(baseRelativeUri);
203-
WaitUntilExists(By.CssSelector("h1#authentication"));
204-
}
205196
}
206197
}

src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
@inject IComponentContext ComponentContext
44

55
<CascadingAuthenticationState>
6-
<h1>Hello</h1>
6+
<AuthorizeView>
7+
<Authorized><h1>Hello, @context.User.Identity.Name!</h1></Authorized>
8+
<NotAuthorized><h1>Hello, anonymous!</h1></NotAuthorized>
9+
</AuthorizeView>
710

811
<p>
912
Current state:

0 commit comments

Comments
 (0)