diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index 99012a31c8..05789adc4f 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.cs @@ -1,6 +1,7 @@ // 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.Security.Claims; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Components.Server.Circuits @@ -11,6 +12,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, - string baseUriAbsolute); + string baseUriAbsolute, + ClaimsPrincipal user); } } diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 897fa4e8fa..d82344e67c 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -109,6 +110,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits }); } + public void SetCircuitUser(ClaimsPrincipal user) + { + var authenticationStateProvider = Services.GetService() as IHostEnvironmentAuthenticationStateProvider; + if (authenticationStateProvider != null) + { + var authenticationState = new AuthenticationState(user); + authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); + } + } + internal void InitializeCircuitAfterPrerender(UnhandledExceptionEventHandler unhandledException) { if (!_initialized) diff --git a/src/Components/Server/src/Circuits/CircuitPrerenderer.cs b/src/Components/Server/src/Circuits/CircuitPrerenderer.cs index 4ac595656c..13917bad58 100644 --- a/src/Components/Server/src/Circuits/CircuitPrerenderer.cs +++ b/src/Components/Server/src/Circuits/CircuitPrerenderer.cs @@ -132,7 +132,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits context, client: new CircuitClientProxy(), // This creates an "offline" client. GetFullUri(context.Request), - GetFullBaseUri(context.Request)); + GetFullBaseUri(context.Request), + context.User); result.UnhandledException += CircuitHost_UnhandledException; context.Response.OnCompleted(() => diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index a33b2f646a..5d5a0118db 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -4,17 +4,16 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Rendering; -using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; -using System.Threading.Tasks; namespace Microsoft.AspNetCore.Components.Server.Circuits { @@ -40,7 +39,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, - string baseUriAbsolute) + string baseUriAbsolute, + ClaimsPrincipal user) { var components = ResolveComponentMetadata(httpContext, client); @@ -51,13 +51,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits jsRuntime.Initialize(client); componentContext.Initialize(client); - var authenticationStateProvider = scope.ServiceProvider.GetService() 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 uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService(); var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService(); if (client.Connected) @@ -102,6 +95,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits // Initialize per - circuit data that services need (circuitHost.Services.GetRequiredService() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit; + circuitHost.SetCircuitUser(user); return circuitHost; } diff --git a/src/Components/Server/src/ComponentHub.cs b/src/Components/Server/src/ComponentHub.cs index a3dddbbc1c..ed796e73a1 100644 --- a/src/Components/Server/src/ComponentHub.cs +++ b/src/Components/Server/src/ComponentHub.cs @@ -97,7 +97,8 @@ namespace Microsoft.AspNetCore.Components.Server Context.GetHttpContext(), circuitClient, uriAbsolute, - baseUriAbsolute); + baseUriAbsolute, + Context.User); circuitHost.UnhandledException += CircuitHost_UnhandledException; @@ -125,6 +126,7 @@ namespace Microsoft.AspNetCore.Components.Server CircuitHost = circuitHost; circuitHost.InitializeCircuitAfterPrerender(CircuitHost_UnhandledException); + circuitHost.SetCircuitUser(Context.User); circuitHost.SendPendingBatches(); return true; } diff --git a/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs b/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs index 2ecf8a7fab..812442facd 100644 --- a/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs +++ b/src/Components/Server/test/Circuits/CircuitPrerendererTest.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Security.Claims; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -190,7 +191,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits _circuitIdFactory = circuitIdFactory ?? (() => Guid.NewGuid().ToString()); } - public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute) + public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute, ClaimsPrincipal user) { var serviceCollection = new ServiceCollection(); serviceCollection.AddScoped(_ => @@ -209,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits public Mock MockServiceScope { get; } = new Mock(); - public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute) + public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute, ClaimsPrincipal user) { return TestCircuitHost.Create(Guid.NewGuid().ToString(), MockServiceScope.Object); } diff --git a/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs b/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs index 62d62f0d68..e866d890d1 100644 --- a/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs +++ b/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs @@ -2,12 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using BasicTestApp; -using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; using OpenQA.Selenium; using OpenQA.Selenium.Support.UI; using System; +using System.Linq; using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure @@ -49,5 +49,32 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure .Until(driver => (result = driver.FindElement(findBy)) != null); return result; } + + protected void SignInAs(string usernameOrNull, string rolesOrNull, bool useSeparateTab = false) + { + const string authenticationPageUrl = "/Authentication"; + var baseRelativeUri = usernameOrNull == null + ? $"{authenticationPageUrl}?signout=true" + : $"{authenticationPageUrl}?username={usernameOrNull}&roles={rolesOrNull}"; + + if (useSeparateTab) + { + // Some tests need to change the authentication state without discarding the + // original page, but this adds several seconds of delay + var javascript = (IJavaScriptExecutor)Browser; + var originalWindow = Browser.CurrentWindowHandle; + javascript.ExecuteScript("window.open()"); + Browser.SwitchTo().Window(Browser.WindowHandles.Last()); + Navigate(baseRelativeUri); + WaitUntilExists(By.CssSelector("h1#authentication")); + javascript.ExecuteScript("window.close()"); + Browser.SwitchTo().Window(originalWindow); + } + else + { + Navigate(baseRelativeUri); + WaitUntilExists(By.CssSelector("h1#authentication")); + } + } } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs index 21c14f9789..a7bbc8b9da 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs @@ -5,6 +5,7 @@ using System; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using BasicTestApp; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.E2ETesting; @@ -14,16 +15,15 @@ using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { - public class PrerenderingTest : ServerTestBase + [Collection("auth")] // Because auth uses cookies, this can't run in parallel with other auth tests + public class PrerenderingTest : BasicTestAppTestBase { public PrerenderingTest( BrowserFixture browserFixture, - AspNetSiteServerFixture serverFixture, + ToggleExecutionModeServerFixture serverFixture, ITestOutputHelper output) - : base(browserFixture, serverFixture, output) + : base(browserFixture, serverFixture.WithServerExecution(), output) { - _serverFixture.Environment = AspNetEnvironment.Development; - _serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost; } [Fact] @@ -97,6 +97,24 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests Assert.Equal(expectedUri, response.Headers.Location); } + [Theory] + [InlineData(null, null)] + [InlineData(null, "Bert")] + [InlineData("Bert", null)] + [InlineData("Bert", "Treb")] + public void CanAccessAuthenticationStateDuringStaticPrerendering(string initialUsername, string interactiveUsername) + { + // See that the authentication state is usable during the initial prerendering + SignInAs(initialUsername, null); + Navigate("/prerendered/prerendered-transition"); + Browser.Equal($"Hello, {initialUsername ?? "anonymous"}!", () => Browser.FindElement(By.TagName("h1")).Text); + + // See that during connection, we update to whatever the latest authentication state now is + SignInAs(interactiveUsername, null, useSeparateTab: true); + BeginInteractivity(); + Browser.Equal($"Hello, {interactiveUsername ?? "anonymous"}!", () => Browser.FindElement(By.TagName("h1")).Text); + } + private void BeginInteractivity() { Browser.FindElement(By.Id("load-boot-script")).Click(); diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerAuthTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerAuthTest.cs new file mode 100644 index 0000000000..a596cbc036 --- /dev/null +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerAuthTest.cs @@ -0,0 +1,65 @@ +// 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 BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.Components.E2ETest.Tests; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using OpenQA.Selenium.Support.UI; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests +{ + public class ServerAuthTest : AuthTest + { + public ServerAuthTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, ITestOutputHelper output) + : base(browserFixture, serverFixture.WithServerExecution(), output) + { + } + + [Theory] + [InlineData(null, null)] + [InlineData(null, "Someone")] + [InlineData("Someone", null)] + [InlineData("Someone", "Someone")] + public void UpdatesAuthenticationStateWhenReconnecting( + string usernameBefore, string usernameAfter) + { + // Establish state before disconnection + SignInAs(usernameBefore, usernameBefore == null ? null : "TestRole"); + var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); + AssertState(usernameBefore); + + // Change authentication state and force reconnection + SignInAs(usernameAfter, usernameAfter == null ? null : "TestRole", useSeparateTab: true); + PerformReconnection(); + AssertState(usernameAfter); + + void AssertState(string username) + { + if (username == null) + { + Browser.Equal("You're not authorized, anonymous", () => + appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text); + } + else + { + Browser.Equal($"Welcome, {username}!", () => + appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text); + } + } + } + + private void PerformReconnection() + { + ((IJavaScriptExecutor)Browser).ExecuteScript("Blazor._internal.forceCloseConnection()"); + + // Wait until the reconnection dialog has been shown but is now hidden + new WebDriverWait(Browser, TimeSpan.FromSeconds(10)) + .Until(driver => driver.FindElement(By.Id("components-reconnect-modal"))?.GetCssValue("display") == "none"); + } + } +} diff --git a/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs b/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs index c99c1d3828..5f22a585a1 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs @@ -83,12 +83,4 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { } } - - public class ServerAuthTest : AuthTest - { - public ServerAuthTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture serverFixture, ITestOutputHelper output) - : base(browserFixture, serverFixture.WithServerExecution(), output) - { - } - } } diff --git a/src/Components/test/E2ETest/Tests/AuthTest.cs b/src/Components/test/E2ETest/Tests/AuthTest.cs index 9c2c26b0c6..deaa6bfb64 100644 --- a/src/Components/test/E2ETest/Tests/AuthTest.cs +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -11,15 +11,16 @@ using Xunit.Abstractions; namespace Microsoft.AspNetCore.Components.E2ETest.Tests { + [Collection("auth")] // Because auth uses cookies, this can't run in parallel with other auth tests public class AuthTest : BasicTestAppTestBase { // These strings correspond to the links in BasicTestApp\AuthTest\Links.razor - const string CascadingAuthenticationStateLink = "Cascading authentication state"; - const string AuthorizeViewCases = "AuthorizeView cases"; - const string PageAllowingAnonymous = "Page allowing anonymous"; - const string PageRequiringAuthorization = "Page requiring any authentication"; - const string PageRequiringPolicy = "Page requiring policy"; - const string PageRequiringRole = "Page requiring role"; + protected const string CascadingAuthenticationStateLink = "Cascading authentication state"; + protected const string AuthorizeViewCases = "AuthorizeView cases"; + protected const string PageAllowingAnonymous = "Page allowing anonymous"; + protected const string PageRequiringAuthorization = "Page requiring any authentication"; + protected const string PageRequiringPolicy = "Page requiring policy"; + protected const string PageRequiringRole = "Page requiring role"; public AuthTest( BrowserFixture browserFixture, @@ -184,7 +185,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests appElement.FindElement(By.CssSelector("#auth-failure")).Text); } - IWebElement MountAndNavigateToAuthTest(string authLinkText) + protected IWebElement MountAndNavigateToAuthTest(string authLinkText) { Navigate(ServerPathBase); var appElement = MountTestComponent(); @@ -192,15 +193,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests appElement.FindElement(By.LinkText(authLinkText)).Click(); return appElement; } - - void SignInAs(string usernameOrNull, string rolesOrNull) - { - const string authenticationPageUrl = "/Authentication"; - var baseRelativeUri = usernameOrNull == null - ? $"{authenticationPageUrl}?signout=true" - : $"{authenticationPageUrl}?username={usernameOrNull}&roles={rolesOrNull}"; - Navigate(baseRelativeUri); - WaitUntilExists(By.CssSelector("h1#authentication")); - } } } diff --git a/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor b/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor index 23dfae3927..af6010e649 100644 --- a/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor +++ b/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor @@ -3,7 +3,10 @@ @inject IComponentContext ComponentContext -

Hello

+ +

Hello, @context.User.Identity.Name!

+

Hello, anonymous!

+

Current state: