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);
}
public partial interface IHostEnvironmentAuthenticationStateProvider
{
void SetAuthenticationState(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> authenticationStateTask);
}
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
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.Logging;
using Microsoft.JSInterop;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
@ -50,9 +51,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
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>();
(authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User);
var authenticationStateProvider = scope.ServiceProvider.GetService<AuthenticationStateProvider>() 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<IUriHelper>();
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<INavigationInterception, RemoteNavigationInterception>();
services.AddScoped<IComponentContext, RemoteComponentContext>();
services.AddScoped<AuthenticationStateProvider, FixedAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
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
@inject IComponentContext ComponentContext
<h1>Hello</h1>
<CascadingAuthenticationState>
<h1>Hello</h1>
<p>
Current state:
<strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong>
</p>
<p>
Current state:
<strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong>
</p>
<p>
Clicks:
<strong id="count">@count</strong>
<button id="increment-count" @onclick="@(() => count++)">Click me</button>
</p>
<p>
Clicks:
<strong id="count">@count</strong>
<button id="increment-count" @onclick="@(() => count++)">Click me</button>
</p>
</CascadingAuthenticationState>
@code {
int count;

View File

@ -7,7 +7,7 @@
<base href="~/" />
</head>
<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
@ -19,17 +19,17 @@
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
</script>
</body>
</html>

View File

@ -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 @@ namespace TestServer
app.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToPage("/PrerenderedHost");
endpoints.MapBlazorHub();
endpoints.MapBlazorHub<TestRouter>(selector: "app");
});
});

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
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 @@ 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
// 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<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
if (authenticationStateProvider != null)
{
var authenticationState = new AuthenticationState(httpContext.User);
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
}
var helper = (UriHelperBase)httpContext.RequestServices.GetRequiredService<IUriHelper>();
helper.InitializeState(GetFullUri(httpContext.Request), GetContextBaseUri(httpContext.Request));
}