diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs index 190f1f6f5f08..d917970d516e 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -359,6 +359,10 @@ public partial interface IHandleEvent { System.Threading.Tasks.Task HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem item, object arg); } + public partial interface IHostEnvironmentAuthenticationStateProvider + { + void SetAuthenticationState(System.Threading.Tasks.Task authenticationStateTask); + } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] public sealed partial class InjectAttribute : System.Attribute { diff --git a/src/Components/Components/src/Auth/IHostEnvironmentAuthenticationStateProvider.cs b/src/Components/Components/src/Auth/IHostEnvironmentAuthenticationStateProvider.cs new file mode 100644 index 000000000000..77e8d022d473 --- /dev/null +++ b/src/Components/Components/src/Auth/IHostEnvironmentAuthenticationStateProvider.cs @@ -0,0 +1,20 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components +{ + /// + /// An interface implemented by classes that can receive authentication + /// state information from the host environment. + /// + public interface IHostEnvironmentAuthenticationStateProvider + { + /// + /// Supplies updated authentication state data to the . + /// + /// A task that resolves with the updated . + void SetAuthenticationState(Task authenticationStateTask); + } +} diff --git a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs index c08db124bcf7..a33b2f646a80 100644 --- a/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs +++ b/src/Components/Server/src/Circuits/DefaultCircuitFactory.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.Components.Server.Circuits { @@ -50,9 +51,12 @@ public override CircuitHost CreateCircuitHost( jsRuntime.Initialize(client); componentContext.Initialize(client); - // You can replace the AuthenticationStateProvider with a custom one, but in that case initialization is up to you - var authenticationStateProvider = scope.ServiceProvider.GetService(); - (authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User); + 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(); diff --git a/src/Components/Server/src/Circuits/FixedAuthenticationStateProvider.cs b/src/Components/Server/src/Circuits/FixedAuthenticationStateProvider.cs deleted file mode 100644 index 41c0942cd86b..000000000000 --- a/src/Components/Server/src/Circuits/FixedAuthenticationStateProvider.cs +++ /dev/null @@ -1,33 +0,0 @@ -// 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.Tasks; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.Components.Server.Circuits -{ - /// - /// An intended for use in server-side - /// Blazor. The circuit factory will supply a from - /// the current , which will stay fixed for the - /// lifetime of the circuit since cannot change. - /// - /// This can therefore only be used with redirect-style authentication flows, - /// since it requires a new HTTP request in order to become a different user. - /// - internal class FixedAuthenticationStateProvider : AuthenticationStateProvider - { - private Task _authenticationStateTask; - - public void Initialize(ClaimsPrincipal user) - { - _authenticationStateTask = Task.FromResult(new AuthenticationState(user)); - } - - public override Task GetAuthenticationStateAsync() - => _authenticationStateTask - ?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(Initialize)}."); - } -} diff --git a/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs b/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs new file mode 100644 index 000000000000..bd1fecfa6821 --- /dev/null +++ b/src/Components/Server/src/Circuits/ServerAuthenticationStateProvider.cs @@ -0,0 +1,26 @@ +// 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.Threading.Tasks; + +namespace Microsoft.AspNetCore.Components.Server.Circuits +{ + /// + /// An intended for use in server-side Blazor. + /// + internal class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider + { + private Task _authenticationStateTask; + + public override Task GetAuthenticationStateAsync() + => _authenticationStateTask + ?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}."); + + public void SetAuthenticationState(Task authenticationStateTask) + { + _authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask)); + NotifyAuthenticationStateChanged(_authenticationStateTask); + } + } +} diff --git a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs index 826410e0d102..7bb89a4144c5 100644 --- a/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs +++ b/src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs @@ -76,7 +76,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.TryAddEnumerable(ServiceDescriptor.Singleton, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); diff --git a/src/Components/Server/test/Circuits/FixedAuthenticationStateProviderTest.cs b/src/Components/Server/test/Circuits/FixedAuthenticationStateProviderTest.cs deleted file mode 100644 index e1a538c9ab85..000000000000 --- a/src/Components/Server/test/Circuits/FixedAuthenticationStateProviderTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.Tasks; -using Microsoft.AspNetCore.Components.Server.Circuits; -using Microsoft.AspNetCore.Http; -using Xunit; - -namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits -{ - public class FixedAuthenticationStateProviderTest - { - [Fact] - public async Task CannotProvideAuthenticationStateBeforeInitialization() - { - await Assert.ThrowsAsync(() => - new FixedAuthenticationStateProvider() - .GetAuthenticationStateAsync()); - } - - [Fact] - public async Task SuppliesAuthenticationStateWithFixedUser() - { - // Arrange - var user = new ClaimsPrincipal(); - var provider = new FixedAuthenticationStateProvider(); - provider.Initialize(user); - - // Act - var authenticationState = await provider.GetAuthenticationStateAsync(); - - // Assert - Assert.NotNull(authenticationState); - Assert.Same(user, authenticationState.User); - } - } -} diff --git a/src/Components/Server/test/Circuits/ServerAuthenticationStateProviderTest.cs b/src/Components/Server/test/Circuits/ServerAuthenticationStateProviderTest.cs new file mode 100644 index 000000000000..6411c25f6b69 --- /dev/null +++ b/src/Components/Server/test/Circuits/ServerAuthenticationStateProviderTest.cs @@ -0,0 +1,49 @@ +// 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.Tasks; +using Microsoft.AspNetCore.Components.Server.Circuits; +using Xunit; + +namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits +{ + public class ServerAuthenticationStateProviderTest + { + [Fact] + public async Task CannotProvideAuthenticationStateBeforeInitialization() + { + await Assert.ThrowsAsync(() => + new ServerAuthenticationStateProvider() + .GetAuthenticationStateAsync()); + } + + [Fact] + public async Task SuppliesAuthenticationStateWithFixedUser() + { + // Arrange + var user = new ClaimsPrincipal(); + var provider = new ServerAuthenticationStateProvider(); + + // Act 1 + var expectedAuthenticationState1 = new AuthenticationState(user); + provider.SetAuthenticationState(Task.FromResult(expectedAuthenticationState1)); + + // Assert 1 + var actualAuthenticationState1 = await provider.GetAuthenticationStateAsync(); + Assert.NotNull(actualAuthenticationState1); + Assert.Same(expectedAuthenticationState1, actualAuthenticationState1); + + // Act 2: Show we can update it further + var expectedAuthenticationState2 = new AuthenticationState(user); + provider.SetAuthenticationState(Task.FromResult(expectedAuthenticationState2)); + + // Assert 2 + var actualAuthenticationState2 = await provider.GetAuthenticationStateAsync(); + Assert.NotNull(actualAuthenticationState2); + Assert.NotSame(actualAuthenticationState1, actualAuthenticationState2); + Assert.Same(expectedAuthenticationState2, actualAuthenticationState2); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor b/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor index 192efbacd0d8..23dfae39278f 100644 --- a/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor +++ b/src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor @@ -2,18 +2,20 @@ @using Microsoft.AspNetCore.Components @inject IComponentContext ComponentContext -

Hello

+ +

Hello

-

- Current state: - @(ComponentContext.IsConnected ? "connected" : "not connected") -

+

+ Current state: + @(ComponentContext.IsConnected ? "connected" : "not connected") +

-

- Clicks: - @count - -

+

+ Clicks: + @count + +

+
@code { int count; diff --git a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml index b2060b92b15d..3282a3f4a5ec 100644 --- a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml +++ b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml @@ -7,7 +7,7 @@ - @(await Html.RenderComponentAsync()) + @(await Html.RenderStaticComponentAsync()) @* So that E2E tests can make assertions about both the prerendered and @@ -19,17 +19,17 @@ diff --git a/src/Components/test/testassets/TestServer/Startup.cs b/src/Components/test/testassets/TestServer/Startup.cs index 146eaa039ebe..c7024786d4e7 100644 --- a/src/Components/test/testassets/TestServer/Startup.cs +++ b/src/Components/test/testassets/TestServer/Startup.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using BasicTestApp; +using BasicTestApp.RouterTest; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -96,7 +97,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapFallbackToPage("/PrerenderedHost"); - endpoints.MapBlazorHub(); + endpoints.MapBlazorHub(selector: "app"); }); }); diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs index 9fa07a3a5c03..cfe1e7caf570 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs @@ -30,7 +30,7 @@ public async Task> PrerenderComponentAsync( HttpContext httpContext, Type componentType) { - InitializeUriHelper(httpContext); + InitializeStandardComponentServices(httpContext); var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof (ILoggerFactory)); using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode)) { @@ -62,15 +62,21 @@ public async Task> PrerenderComponentAsync( } } - private void InitializeUriHelper(HttpContext httpContext) + private void InitializeStandardComponentServices(HttpContext httpContext) { - // We don't know here if we are dealing with the default HttpUriHelper registered - // by MVC or with the RemoteUriHelper registered by AddComponents. // This might not be the first component in the request we are rendering, so - // we need to check if we already initialized the uri helper in this request. + // we need to check if we already initialized the services in this request. if (!_initialized) { _initialized = true; + + var authenticationStateProvider = httpContext.RequestServices.GetService() as IHostEnvironmentAuthenticationStateProvider; + if (authenticationStateProvider != null) + { + var authenticationState = new AuthenticationState(httpContext.User); + authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState)); + } + var helper = (UriHelperBase)httpContext.RequestServices.GetRequiredService(); helper.InitializeState(GetFullUri(httpContext.Request), GetContextBaseUri(httpContext.Request)); }