Make static prerendering support auth. Fixes #11799 (#12318)

* Make E2E prerendering test use static prerendering (we no longer need coverage for stateful prerendering)

* Use authentication state during static prerendering. This replicates issue #11799 in the E2E test

* Initialize the authentication state provider during static prerendering

* Update ref assembly

* Update unit test
This commit is contained in:
Steve Sanderson 2019-07-19 19:26:33 +01:00 committed by GitHub
parent c25a0d1bf5
commit 1950e59714
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 143 additions and 103 deletions

View File

@ -359,6 +359,10 @@ namespace Microsoft.AspNetCore.Components
{ {
System.Threading.Tasks.Task HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem item, object arg); System.Threading.Tasks.Task HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem item, object arg);
} }
public partial interface IHostEnvironmentAuthenticationStateProvider
{
void SetAuthenticationState(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> authenticationStateTask);
}
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)] [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
public sealed partial class InjectAttribute : System.Attribute public sealed partial class InjectAttribute : System.Attribute
{ {

View File

@ -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
{
/// <summary>
/// An interface implemented by <see cref="AuthenticationStateProvider"/> classes that can receive authentication
/// state information from the host environment.
/// </summary>
public interface IHostEnvironmentAuthenticationStateProvider
{
/// <summary>
/// Supplies updated authentication state data to the <see cref="AuthenticationStateProvider"/>.
/// </summary>
/// <param name="authenticationStateTask">A task that resolves with the updated <see cref="AuthenticationState"/>.</param>
void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask);
}
}

View File

@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Server.Circuits namespace Microsoft.AspNetCore.Components.Server.Circuits
{ {
@ -50,9 +51,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
jsRuntime.Initialize(client); jsRuntime.Initialize(client);
componentContext.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 IHostEnvironmentAuthenticationStateProvider;
var authenticationStateProvider = scope.ServiceProvider.GetService<AuthenticationStateProvider>(); if (authenticationStateProvider != null)
(authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User); {
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<IUriHelper>(); var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>(); var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();

View File

@ -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
{
/// <summary>
/// An <see cref="AuthenticationStateProvider"/> intended for use in server-side
/// Blazor. The circuit factory will supply a <see cref="ClaimsPrincipal"/> from
/// the current <see cref="HttpContext.User"/>, which will stay fixed for the
/// lifetime of the circuit since <see cref="HttpContext.User"/> 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.
/// </summary>
internal class FixedAuthenticationStateProvider : AuthenticationStateProvider
{
private Task<AuthenticationState> _authenticationStateTask;
public void Initialize(ClaimsPrincipal user)
{
_authenticationStateTask = Task.FromResult(new AuthenticationState(user));
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
=> _authenticationStateTask
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(Initialize)}.");
}
}

View File

@ -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
{
/// <summary>
/// An <see cref="AuthenticationStateProvider"/> intended for use in server-side Blazor.
/// </summary>
internal class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
{
private Task<AuthenticationState> _authenticationStateTask;
public override Task<AuthenticationState> GetAuthenticationStateAsync()
=> _authenticationStateTask
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}.");
public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
{
_authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));
NotifyAuthenticationStateChanged(_authenticationStateTask);
}
}
}

View File

@ -76,7 +76,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IJSRuntime, RemoteJSRuntime>(); services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<INavigationInterception, RemoteNavigationInterception>(); services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
services.AddScoped<IComponentContext, RemoteComponentContext>(); services.AddScoped<IComponentContext, RemoteComponentContext>();
services.AddScoped<AuthenticationStateProvider, FixedAuthenticationStateProvider>(); services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>()); services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());

View File

@ -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<InvalidOperationException>(() =>
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);
}
}
}

View File

@ -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<InvalidOperationException>(() =>
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);
}
}
}

View File

@ -2,18 +2,20 @@
@using Microsoft.AspNetCore.Components @using Microsoft.AspNetCore.Components
@inject IComponentContext ComponentContext @inject IComponentContext ComponentContext
<h1>Hello</h1> <CascadingAuthenticationState>
<h1>Hello</h1>
<p> <p>
Current state: Current state:
<strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong> <strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong>
</p> </p>
<p> <p>
Clicks: Clicks:
<strong id="count">@count</strong> <strong id="count">@count</strong>
<button id="increment-count" @onclick="@(() => count++)">Click me</button> <button id="increment-count" @onclick="@(() => count++)">Click me</button>
</p> </p>
</CascadingAuthenticationState>
@code { @code {
int count; int count;

View File

@ -7,7 +7,7 @@
<base href="~/" /> <base href="~/" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<TestRouter>())</app> <app>@(await Html.RenderStaticComponentAsync<TestRouter>())</app>
@* @*
So that E2E tests can make assertions about both the prerendered and So that E2E tests can make assertions about both the prerendered and
@ -19,17 +19,17 @@
<script src="_framework/blazor.server.js" autostart="false"></script> <script src="_framework/blazor.server.js" autostart="false"></script>
<script> <script>
// Used by InteropOnInitializationComponent // Used by InteropOnInitializationComponent
function setElementValue(element, newValue) { function setElementValue(element, newValue) {
element.value = newValue; element.value = newValue;
return element.value; return element.value;
} }
function start() { function start() {
Blazor.start({ Blazor.start({
logLevel: 1 // LogLevel.Debug logLevel: 1 // LogLevel.Debug
}); });
} }
</script> </script>
</body> </body>
</html> </html>

View File

@ -1,5 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BasicTestApp; using BasicTestApp;
using BasicTestApp.RouterTest;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -96,7 +97,7 @@ namespace TestServer
app.UseEndpoints(endpoints => app.UseEndpoints(endpoints =>
{ {
endpoints.MapFallbackToPage("/PrerenderedHost"); endpoints.MapFallbackToPage("/PrerenderedHost");
endpoints.MapBlazorHub(); endpoints.MapBlazorHub<TestRouter>(selector: "app");
}); });
}); });

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
HttpContext httpContext, HttpContext httpContext,
Type componentType) Type componentType)
{ {
InitializeUriHelper(httpContext); InitializeStandardComponentServices(httpContext);
var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof (ILoggerFactory)); var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof (ILoggerFactory));
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode)) using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode))
{ {
@ -62,15 +62,21 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
} }
} }
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 // 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) if (!_initialized)
{ {
_initialized = true; _initialized = true;
var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
if (authenticationStateProvider != null)
{
var authenticationState = new AuthenticationState(httpContext.User);
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
}
var helper = (UriHelperBase)httpContext.RequestServices.GetRequiredService<IUriHelper>(); var helper = (UriHelperBase)httpContext.RequestServices.GetRequiredService<IUriHelper>();
helper.InitializeState(GetFullUri(httpContext.Request), GetContextBaseUri(httpContext.Request)); helper.InitializeState(GetFullUri(httpContext.Request), GetContextBaseUri(httpContext.Request));
} }