Update user on reconnect. Fixes #12051 (#12421)

This commit is contained in:
Steve Sanderson 2019-07-24 10:23:38 -07:00 committed by GitHub
parent 9b6f10d20c
commit 8b7fcf1f76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 154 additions and 47 deletions

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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; using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Components.Server.Circuits namespace Microsoft.AspNetCore.Components.Server.Circuits
@ -11,6 +12,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
HttpContext httpContext, HttpContext httpContext,
CircuitClientProxy client, CircuitClientProxy client,
string uriAbsolute, string uriAbsolute,
string baseUriAbsolute); string baseUriAbsolute,
ClaimsPrincipal user);
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -109,6 +110,16 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
}); });
} }
public void SetCircuitUser(ClaimsPrincipal user)
{
var authenticationStateProvider = Services.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
if (authenticationStateProvider != null)
{
var authenticationState = new AuthenticationState(user);
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
}
}
internal void InitializeCircuitAfterPrerender(UnhandledExceptionEventHandler unhandledException) internal void InitializeCircuitAfterPrerender(UnhandledExceptionEventHandler unhandledException)
{ {
if (!_initialized) if (!_initialized)

View File

@ -132,7 +132,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
context, context,
client: new CircuitClientProxy(), // This creates an "offline" client. client: new CircuitClientProxy(), // This creates an "offline" client.
GetFullUri(context.Request), GetFullUri(context.Request),
GetFullBaseUri(context.Request)); GetFullBaseUri(context.Request),
context.User);
result.UnhandledException += CircuitHost_UnhandledException; result.UnhandledException += CircuitHost_UnhandledException;
context.Response.OnCompleted(() => context.Response.OnCompleted(() =>

View File

@ -4,17 +4,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Rendering; using Microsoft.AspNetCore.Components.Web.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; 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
{ {
@ -40,7 +39,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
HttpContext httpContext, HttpContext httpContext,
CircuitClientProxy client, CircuitClientProxy client,
string uriAbsolute, string uriAbsolute,
string baseUriAbsolute) string baseUriAbsolute,
ClaimsPrincipal user)
{ {
var components = ResolveComponentMetadata(httpContext, client); var components = ResolveComponentMetadata(httpContext, client);
@ -51,13 +51,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
jsRuntime.Initialize(client); jsRuntime.Initialize(client);
componentContext.Initialize(client); componentContext.Initialize(client);
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 uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>(); var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
if (client.Connected) if (client.Connected)
@ -102,6 +95,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// Initialize per - circuit data that services need // Initialize per - circuit data that services need
(circuitHost.Services.GetRequiredService<ICircuitAccessor>() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit; (circuitHost.Services.GetRequiredService<ICircuitAccessor>() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit;
circuitHost.SetCircuitUser(user);
return circuitHost; return circuitHost;
} }

View File

@ -97,7 +97,8 @@ namespace Microsoft.AspNetCore.Components.Server
Context.GetHttpContext(), Context.GetHttpContext(),
circuitClient, circuitClient,
uriAbsolute, uriAbsolute,
baseUriAbsolute); baseUriAbsolute,
Context.User);
circuitHost.UnhandledException += CircuitHost_UnhandledException; circuitHost.UnhandledException += CircuitHost_UnhandledException;
@ -125,6 +126,7 @@ namespace Microsoft.AspNetCore.Components.Server
CircuitHost = circuitHost; CircuitHost = circuitHost;
circuitHost.InitializeCircuitAfterPrerender(CircuitHost_UnhandledException); circuitHost.InitializeCircuitAfterPrerender(CircuitHost_UnhandledException);
circuitHost.SetCircuitUser(Context.User);
circuitHost.SendPendingBatches(); circuitHost.SendPendingBatches();
return true; return true;
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -190,7 +191,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
_circuitIdFactory = circuitIdFactory ?? (() => Guid.NewGuid().ToString()); _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(); var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IUriHelper>(_ => serviceCollection.AddScoped<IUriHelper>(_ =>
@ -209,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
public Mock<IServiceScope> MockServiceScope { get; } public Mock<IServiceScope> MockServiceScope { get; }
= new Mock<IServiceScope>(); = new Mock<IServiceScope>();
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); return TestCircuitHost.Create(Guid.NewGuid().ToString(), MockServiceScope.Object);
} }

View File

@ -2,12 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using BasicTestApp; using BasicTestApp;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting; using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium; using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Support.UI;
using System; using System;
using System.Linq;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
@ -49,5 +49,32 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
.Until(driver => (result = driver.FindElement(findBy)) != null); .Until(driver => (result = driver.FindElement(findBy)) != null);
return result; 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"));
}
}
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting; using Microsoft.AspNetCore.E2ETesting;
@ -14,16 +15,15 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
public class PrerenderingTest : ServerTestBase<AspNetSiteServerFixture> [Collection("auth")] // Because auth uses cookies, this can't run in parallel with other auth tests
public class PrerenderingTest : BasicTestAppTestBase
{ {
public PrerenderingTest( public PrerenderingTest(
BrowserFixture browserFixture, BrowserFixture browserFixture,
AspNetSiteServerFixture serverFixture, ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output) ITestOutputHelper output)
: base(browserFixture, serverFixture, output) : base(browserFixture, serverFixture.WithServerExecution(), output)
{ {
_serverFixture.Environment = AspNetEnvironment.Development;
_serverFixture.BuildWebHostMethod = TestServer.Program.BuildWebHost;
} }
[Fact] [Fact]
@ -97,6 +97,24 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.Equal(expectedUri, response.Headers.Location); 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() private void BeginInteractivity()
{ {
Browser.FindElement(By.Id("load-boot-script")).Click(); Browser.FindElement(By.Id("load-boot-script")).Click();

View File

@ -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<Program> 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");
}
}
}

View File

@ -83,12 +83,4 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{ {
} }
} }
public class ServerAuthTest : AuthTest
{
public ServerAuthTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}
}
} }

View File

@ -11,15 +11,16 @@ using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests 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 public class AuthTest : BasicTestAppTestBase
{ {
// These strings correspond to the links in BasicTestApp\AuthTest\Links.razor // These strings correspond to the links in BasicTestApp\AuthTest\Links.razor
const string CascadingAuthenticationStateLink = "Cascading authentication state"; protected const string CascadingAuthenticationStateLink = "Cascading authentication state";
const string AuthorizeViewCases = "AuthorizeView cases"; protected const string AuthorizeViewCases = "AuthorizeView cases";
const string PageAllowingAnonymous = "Page allowing anonymous"; protected const string PageAllowingAnonymous = "Page allowing anonymous";
const string PageRequiringAuthorization = "Page requiring any authentication"; protected const string PageRequiringAuthorization = "Page requiring any authentication";
const string PageRequiringPolicy = "Page requiring policy"; protected const string PageRequiringPolicy = "Page requiring policy";
const string PageRequiringRole = "Page requiring role"; protected const string PageRequiringRole = "Page requiring role";
public AuthTest( public AuthTest(
BrowserFixture browserFixture, BrowserFixture browserFixture,
@ -184,7 +185,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
appElement.FindElement(By.CssSelector("#auth-failure")).Text); appElement.FindElement(By.CssSelector("#auth-failure")).Text);
} }
IWebElement MountAndNavigateToAuthTest(string authLinkText) protected IWebElement MountAndNavigateToAuthTest(string authLinkText)
{ {
Navigate(ServerPathBase); Navigate(ServerPathBase);
var appElement = MountTestComponent<BasicTestApp.AuthTest.AuthRouter>(); var appElement = MountTestComponent<BasicTestApp.AuthTest.AuthRouter>();
@ -192,15 +193,5 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
appElement.FindElement(By.LinkText(authLinkText)).Click(); appElement.FindElement(By.LinkText(authLinkText)).Click();
return appElement; 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"));
}
} }
} }

View File

@ -3,7 +3,10 @@
@inject IComponentContext ComponentContext @inject IComponentContext ComponentContext
<CascadingAuthenticationState> <CascadingAuthenticationState>
<h1>Hello</h1> <AuthorizeView>
<Authorized><h1>Hello, @context.User.Identity.Name!</h1></Authorized>
<NotAuthorized><h1>Hello, anonymous!</h1></NotAuthorized>
</AuthorizeView>
<p> <p>
Current state: Current state: