From 147880f79656385cc94752e46d05ba2265eaea48 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 20 May 2019 15:41:02 +0100 Subject: [PATCH] Components auth step 2 (#10293) * CR feedback left over from #10227 * Begin adding E2E test case * Add cookie auth and test login page * Make E2E auth component work client-side too * Restructure auth E2E tests around a router so there can easily be multiple such test components * Add E2E test case for AuthorizeView * Prepare for E2E test implementations * Fix ToBaseRelativePath handling of hashes ... otherwise E2E test will fail, because we're using the hash to control server-or-client execution * Decouple E2E execution mode from hosting mode * Actual E2E tests for cascading authentication state * Actual E2E tests for AuthorizeView (in "no authentication rule" mode) * Fix inconsistent namespace * CR: Manual ref assembly definitions for AuthorizeView/CascadingAuthenticationState --- eng/GenAPI.exclusions.txt | 2 + .../Blazor/test/WebAssemblyUriHelperTest.cs | 3 + ...etCore.Components.netstandard2.0.Manual.cs | 26 ++++++ ...ft.AspNetCore.Components.netstandard2.0.cs | 24 ----- .../src/Auth/AuthenticationState.cs | 10 +- .../src/Auth/AuthenticationStateProvider.cs | 7 +- .../Components/src/UriHelperBase.cs | 7 +- .../Infrastructure/BasicTestAppTestBase.cs | 2 +- .../ToggleExecutionModeServerFixture.cs | 6 +- .../ServerExecutionTests/PrerenderingTest.cs | 2 +- .../ServerExecutionTestExtensions.cs | 1 + .../ServerExecutionTests/TestSubclasses.cs | 8 ++ src/Components/test/E2ETest/Tests/AuthTest.cs | 92 +++++++++++++++++++ src/Components/test/E2ETest/Tests/BindTest.cs | 2 +- .../test/E2ETest/Tests/CascadingValueTest.cs | 2 +- .../E2ETest/Tests/ComponentRenderingTest.cs | 2 +- .../test/E2ETest/Tests/EventBubblingTest.cs | 2 +- .../test/E2ETest/Tests/EventCallbackTest.cs | 2 +- .../test/E2ETest/Tests/FormsTest.cs | 2 +- .../test/E2ETest/Tests/InteropTest.cs | 2 +- src/Components/test/E2ETest/Tests/KeyTest.cs | 2 +- .../BasicTestApp/AuthTest/AuthHome.razor | 3 + .../BasicTestApp/AuthTest/AuthRouter.razor | 30 ++++++ .../AuthTest/AuthorizeViewCases.razor | 17 ++++ ...CascadingAuthenticationStateConsumer.razor | 47 ++++++++++ .../ClientSideAuthenticationStateData.cs | 17 ++++ .../BasicTestApp/AuthTest/Links.razor | 8 ++ .../ServerAuthenticationStateProvider.cs | 43 +++++++++ .../test/testassets/BasicTestApp/Index.razor | 2 + .../test/testassets/BasicTestApp/Startup.cs | 3 + .../TestServer/Components.TestServer.csproj | 3 +- .../TestServer/Controllers/UserController.cs | 28 ++++++ .../TestServer/Pages/Authentication.cshtml | 72 +++++++++++++++ .../test/testassets/TestServer/Startup.cs | 4 + 34 files changed, 434 insertions(+), 49 deletions(-) create mode 100644 src/Components/test/E2ETest/Tests/AuthTest.cs create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/AuthHome.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/AuthorizeViewCases.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/ClientSideAuthenticationStateData.cs create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor create mode 100644 src/Components/test/testassets/BasicTestApp/AuthTest/ServerAuthenticationStateProvider.cs create mode 100644 src/Components/test/testassets/TestServer/Controllers/UserController.cs create mode 100644 src/Components/test/testassets/TestServer/Pages/Authentication.cshtml diff --git a/eng/GenAPI.exclusions.txt b/eng/GenAPI.exclusions.txt index 61d8c412b2..4595fc772a 100644 --- a/eng/GenAPI.exclusions.txt +++ b/eng/GenAPI.exclusions.txt @@ -4,6 +4,8 @@ T:Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame T:Microsoft.AspNetCore.Mvc.ApplicationModels.PageParameterModel T:Microsoft.AspNetCore.Mvc.ApplicationModels.PagePropertyModel # Manually implemented - https://github.com/aspnet/AspNetCore/issues/8825 +T:Microsoft.AspNetCore.Components.AuthorizeView +T:Microsoft.AspNetCore.Components.CascadingAuthenticationState T:Microsoft.AspNetCore.Components.CascadingValue`1 T:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator T:Microsoft.AspNetCore.Components.Forms.EditForm diff --git a/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs b/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs index 45c94a71d2..2903d7fcd3 100644 --- a/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs +++ b/src/Components/Blazor/Blazor/test/WebAssemblyUriHelperTest.cs @@ -29,6 +29,9 @@ namespace Microsoft.AspNetCore.Blazor.Services.Test [InlineData("scheme://host/path/", "scheme://host/path/", "")] [InlineData("scheme://host/path/", "scheme://host/path/more", "more")] [InlineData("scheme://host/path/", "scheme://host/path", "")] + [InlineData("scheme://host/path/", "scheme://host/path#hash", "#hash")] + [InlineData("scheme://host/path/", "scheme://host/path/#hash", "#hash")] + [InlineData("scheme://host/path/", "scheme://host/path/more#hash", "more#hash")] public void ComputesCorrectValidBaseRelativePaths(string baseUri, string absoluteUri, string expectedResult) { var actualResult = _uriHelper.ToBaseRelativePath(baseUri, absoluteUri); diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs index 19c9249e47..72db875d48 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs @@ -49,6 +49,32 @@ namespace Microsoft.AspNetCore.Components.RenderTree // Built-in components: https://github.com/aspnet/AspNetCore/issues/8825 namespace Microsoft.AspNetCore.Components { + public partial class AuthorizeView : Microsoft.AspNetCore.Components.ComponentBase + { + public AuthorizeView() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment Authorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + [System.Diagnostics.DebuggerStepThroughAttribute] + protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; } + } + + public partial class CascadingAuthenticationState : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable + { + public CascadingAuthenticationState() { } + [Microsoft.AspNetCore.Components.ParameterAttribute] + public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } } + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } + protected override void OnInit() { } + void System.IDisposable.Dispose() { } + } + public partial class CascadingValue : Microsoft.AspNetCore.Components.IComponent { public CascadingValue() { } 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 1729a18ed1..a1b3d1e2f8 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs @@ -16,21 +16,6 @@ namespace Microsoft.AspNetCore.Components public abstract System.Threading.Tasks.Task GetAuthenticationStateAsync(); protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task task) { } } - public partial class AuthorizeView : Microsoft.AspNetCore.Components.ComponentBase - { - public AuthorizeView() { } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } - [System.Diagnostics.DebuggerStepThroughAttribute] - protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; } - } [Microsoft.AspNetCore.Components.BindElementAttribute("select", null, "value", "onchange")] [Microsoft.AspNetCore.Components.BindElementAttribute("textarea", null, "value", "onchange")] [Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange")] @@ -85,15 +70,6 @@ namespace Microsoft.AspNetCore.Components public static System.Action SetValueHandler(System.Action setter, string existingValue) { throw null; } public static System.Action SetValueHandler(System.Action setter, T existingValue) { throw null; } } - public partial class CascadingAuthenticationState : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable - { - public CascadingAuthenticationState() { } - [Microsoft.AspNetCore.Components.ParameterAttribute] - public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { } - protected override void OnInit() { } - void System.IDisposable.Dispose() { } - } [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=false)] public sealed partial class CascadingParameterAttribute : System.Attribute { diff --git a/src/Components/Components/src/Auth/AuthenticationState.cs b/src/Components/Components/src/Auth/AuthenticationState.cs index 2a145d44f3..d90090c7c8 100644 --- a/src/Components/Components/src/Auth/AuthenticationState.cs +++ b/src/Components/Components/src/Auth/AuthenticationState.cs @@ -11,11 +11,6 @@ namespace Microsoft.AspNetCore.Components /// public class AuthenticationState { - /// - /// Gets a that describes the current user. - /// - public ClaimsPrincipal User { get; } - /// /// Constructs an instance of . /// @@ -24,5 +19,10 @@ namespace Microsoft.AspNetCore.Components { User = user ?? throw new ArgumentNullException(nameof(user)); } + + /// + /// Gets a that describes the current user. + /// + public ClaimsPrincipal User { get; } } } diff --git a/src/Components/Components/src/Auth/AuthenticationStateProvider.cs b/src/Components/Components/src/Auth/AuthenticationStateProvider.cs index ffd2fc252a..e9e3e3c772 100644 --- a/src/Components/Components/src/Auth/AuthenticationStateProvider.cs +++ b/src/Components/Components/src/Auth/AuthenticationStateProvider.cs @@ -12,19 +12,16 @@ namespace Microsoft.AspNetCore.Components public abstract class AuthenticationStateProvider { /// - /// Gets an instance that describes - /// the current user. + /// Asynchronously gets an that describes the current user. /// - /// An instance that describes the current user. + /// A task that, when resolved, gives an instance that describes the current user. public abstract Task GetAuthenticationStateAsync(); /// /// An event that provides notification when the /// has changed. For example, this event may be raised if a user logs in or out. /// -#pragma warning disable 0067 // "Never used" (it's only raised by subclasses) public event AuthenticationStateChangedHandler AuthenticationStateChanged; -#pragma warning restore 0067 /// /// Raises the event. diff --git a/src/Components/Components/src/UriHelperBase.cs b/src/Components/Components/src/UriHelperBase.cs index 41af519eca..3b8f091045 100644 --- a/src/Components/Components/src/UriHelperBase.cs +++ b/src/Components/Components/src/UriHelperBase.cs @@ -154,7 +154,10 @@ namespace Microsoft.AspNetCore.Components // baseUri ends with a slash), and from that we return "something" return locationAbsolute.Substring(baseUri.Length); } - else if ($"{locationAbsolute}/".Equals(baseUri, StringComparison.Ordinal)) + + var hashIndex = locationAbsolute.IndexOf('#'); + var locationAbsoluteNoHash = hashIndex < 0 ? locationAbsolute : locationAbsolute.Substring(0, hashIndex); + if ($"{locationAbsoluteNoHash}/".Equals(baseUri, StringComparison.Ordinal)) { // Special case: for the base URI "/something/", if you're at // "/something" then treat it as if you were at "/something/" (i.e., @@ -162,7 +165,7 @@ namespace Microsoft.AspNetCore.Components // whether the server would return the same page whether or not the // slash is present, but ASP.NET Core at least does by default when // using PathBase. - return string.Empty; + return locationAbsolute.Substring(baseUri.Length - 1); } var message = $"The URI '{locationAbsolute}' is not contained by the base URI '{baseUri}'."; diff --git a/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs b/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs index d4933b858e..62d62f0d68 100644 --- a/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs +++ b/src/Components/test/E2ETest/Infrastructure/BasicTestAppTestBase.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure public class BasicTestAppTestBase : ServerTestBase> { public string ServerPathBase - => "/subdir" + (_serverFixture.UsingAspNetHost ? "#server" : ""); + => "/subdir" + (_serverFixture.ExecutionMode == ExecutionMode.Server ? "#server" : ""); public BasicTestAppTestBase( BrowserFixture browserFixture, diff --git a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs index 9fb9863f60..759b871852 100644 --- a/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs +++ b/src/Components/test/E2ETest/Infrastructure/ServerFixtures/ToggleExecutionModeServerFixture.cs @@ -9,7 +9,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures : ServerFixture { public string PathBase { get; set; } - public bool UsingAspNetHost { get; private set; } + + public ExecutionMode ExecutionMode { get; set; } = ExecutionMode.Client; private AspNetSiteServerFixture.BuildWebHost _buildWebHostMethod; private IDisposable _serverToDispose; @@ -18,7 +19,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures { _buildWebHostMethod = buildWebHostMethod ?? throw new ArgumentNullException(nameof(buildWebHostMethod)); - UsingAspNetHost = true; } protected override string StartAndGetRootUri() @@ -46,4 +46,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures _serverToDispose?.Dispose(); } } + + public enum ExecutionMode { Client, Server } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs index be5e996196..b7b6d361c8 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/PrerenderingTest.cs @@ -8,7 +8,7 @@ using OpenQA.Selenium; using Xunit; using Xunit.Abstractions; -namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests +namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests { public class PrerenderingTest : ServerTestBase { diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs b/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs index e867e03a9b..c8e34f36cd 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/ServerExecutionTestExtensions.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests public static ToggleExecutionModeServerFixture WithServerExecution(this ToggleExecutionModeServerFixture serverFixture) { serverFixture.UseAspNetHost(TestServer.Program.BuildWebHost); + serverFixture.ExecutionMode = ExecutionMode.Server; return serverFixture; } } diff --git a/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs b/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs index 9265718d54..1f16095af5 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs @@ -81,4 +81,12 @@ 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 new file mode 100644 index 0000000000..cc3139b6ab --- /dev/null +++ b/src/Components/test/E2ETest/Tests/AuthTest.cs @@ -0,0 +1,92 @@ +// 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 BasicTestApp; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETest.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"; + + public AuthTest( + BrowserFixture browserFixture, + ToggleExecutionModeServerFixture serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + // Normally, the E2E tests use the Blazor dev server if they are testing + // client-side execution. But for the auth tests, we always have to run + // in "hosted on ASP.NET Core" mode, because we get the auth state from it. + serverFixture.UseAspNetHost(TestServer.Program.BuildWebHost); + } + + [Fact] + public void CascadingAuthenticationState_Unauthenticated() + { + SignInAs(null); + + var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink); + + Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text); + Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text); + } + + [Fact] + public void CascadingAuthenticationState_Authenticated() + { + SignInAs("someone cool"); + + var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink); + + Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text); + Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text); + Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text); + } + + [Fact] + public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated() + { + SignInAs(null); + MountAndNavigateToAuthTest(AuthorizeViewCases); + WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized")); + } + + [Fact] + public void AuthorizeViewCases_NoAuthorizationRule_Authenticated() + { + SignInAs("Some User"); + var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases); + Browser.Equal("Welcome, Some User!", () => + appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text); + } + + IWebElement MountAndNavigateToAuthTest(string authLinkText) + { + Navigate(ServerPathBase); + var appElement = MountTestComponent(); + WaitUntilExists(By.Id("auth-links")); + appElement.FindElement(By.LinkText(authLinkText)).Click(); + return appElement; + } + + void SignInAs(string usernameOrNull) + { + const string authenticationPageUrl = "/Authentication"; + var baseRelativeUri = usernameOrNull == null + ? $"{authenticationPageUrl}?signout=true" + : $"{authenticationPageUrl}?username={usernameOrNull}"; + Navigate(baseRelativeUri); + WaitUntilExists(By.CssSelector("h1#authentication")); + } + } +} diff --git a/src/Components/test/E2ETest/Tests/BindTest.cs b/src/Components/test/E2ETest/Tests/BindTest.cs index b08a315441..51fcc9ea9d 100644 --- a/src/Components/test/E2ETest/Tests/BindTest.cs +++ b/src/Components/test/E2ETest/Tests/BindTest.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); WaitUntilExists(By.Id("bind-cases")); } diff --git a/src/Components/test/E2ETest/Tests/CascadingValueTest.cs b/src/Components/test/E2ETest/Tests/CascadingValueTest.cs index 8102be1e58..e4642fa98c 100644 --- a/src/Components/test/E2ETest/Tests/CascadingValueTest.cs +++ b/src/Components/test/E2ETest/Tests/CascadingValueTest.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); } diff --git a/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs b/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs index 5c8e194cac..c88d2997c6 100644 --- a/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs +++ b/src/Components/test/E2ETest/Tests/ComponentRenderingTest.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); } [Fact] diff --git a/src/Components/test/E2ETest/Tests/EventBubblingTest.cs b/src/Components/test/E2ETest/Tests/EventBubblingTest.cs index a15d1b31d4..f45e3d106a 100644 --- a/src/Components/test/E2ETest/Tests/EventBubblingTest.cs +++ b/src/Components/test/E2ETest/Tests/EventBubblingTest.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); WaitUntilExists(By.Id("event-bubbling")); } diff --git a/src/Components/test/E2ETest/Tests/EventCallbackTest.cs b/src/Components/test/E2ETest/Tests/EventCallbackTest.cs index 4e5472a88a..01d53d6b20 100644 --- a/src/Components/test/E2ETest/Tests/EventCallbackTest.cs +++ b/src/Components/test/E2ETest/Tests/EventCallbackTest.cs @@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); MountTestComponent(); } diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index 88f2a0d9fb..9f1083af20 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); } [Fact] diff --git a/src/Components/test/E2ETest/Tests/InteropTest.cs b/src/Components/test/E2ETest/Tests/InteropTest.cs index 4b82c98096..7ab262c465 100644 --- a/src/Components/test/E2ETest/Tests/InteropTest.cs +++ b/src/Components/test/E2ETest/Tests/InteropTest.cs @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests // Include the sync assertions only when running under WebAssembly var expectedValues = expectedAsyncValues; - if (!_serverFixture.UsingAspNetHost) + if (_serverFixture.ExecutionMode == ExecutionMode.Client) { foreach (var kvp in expectedSyncValues) { diff --git a/src/Components/test/E2ETest/Tests/KeyTest.cs b/src/Components/test/E2ETest/Tests/KeyTest.cs index b7a5519694..2cb0e58d18 100644 --- a/src/Components/test/E2ETest/Tests/KeyTest.cs +++ b/src/Components/test/E2ETest/Tests/KeyTest.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests protected override void InitializeAsyncCore() { // On WebAssembly, page reloads are expensive so skip if possible - Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost); + Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client); } [Fact] diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthHome.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthHome.razor new file mode 100644 index 0000000000..863f865a98 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthHome.razor @@ -0,0 +1,3 @@ +@page "/AuthHome" + +Select an auth test below. diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor new file mode 100644 index 0000000000..299a5beb87 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthRouter.razor @@ -0,0 +1,30 @@ +@using Microsoft.AspNetCore.Components.Routing +@inject IUriHelper UriHelper + +@* + This router is independent of any other router that may exist within the same project. + It exists so that (1) we can easily have multiple test cases that depend on the + CascadingAuthenticationState, and (2) we can test the integration between the router + and @page authorization rules. +*@ + + + + + +
+ + +@functions { + protected override void OnInit() + { + // Start at AuthHome, not at any other component in the same app that happens to + // register itself for the route "" + var absoluteUriPath = new Uri(UriHelper.GetAbsoluteUri()).GetLeftPart(UriPartial.Path); + var relativeUri = UriHelper.ToBaseRelativePath(UriHelper.GetBaseUri(), absoluteUriPath); + if (relativeUri == string.Empty) + { + UriHelper.NavigateTo("AuthHome"); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/AuthorizeViewCases.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthorizeViewCases.razor new file mode 100644 index 0000000000..d78c70f72a --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/AuthorizeViewCases.razor @@ -0,0 +1,17 @@ +@page "/AuthorizeViewCases" + +
+

Scenario: No authorization rule

+ + + +

Authorizing...

+
+ +

Welcome, @context.User.Identity.Name!

+
+ +

You're not logged in.

+
+
+
diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor new file mode 100644 index 0000000000..8113e1080f --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/CascadingAuthenticationStateConsumer.razor @@ -0,0 +1,47 @@ +@page "/CascadingAuthenticationStateConsumer" +@using System.Security.Claims + +

Cascading authentication state

+ +@if (user == null) +{ + Requesting authentication state... +} +else +{ +

+ Authenticated: + @user.Identity.IsAuthenticated +

+ +

+ Name: + @user.Identity.Name +

+ +

+ Test claim: + @if (user.HasClaim(TestClaimPredicate) == true) + { + @user.Claims.Single(c => TestClaimPredicate(c)).Value + } + else + { + (none) + } +

+} + +@functions +{ + static Predicate TestClaimPredicate = c => c.Type == "test-claim"; + + ClaimsPrincipal user; + + [CascadingParameter] Task AuthenticationStateTask { get; set; } + + protected override async Task OnParametersSetAsync() + { + user = (await AuthenticationStateTask).User; + } +} diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/ClientSideAuthenticationStateData.cs b/src/Components/test/testassets/BasicTestApp/AuthTest/ClientSideAuthenticationStateData.cs new file mode 100644 index 0000000000..15178b5d82 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/ClientSideAuthenticationStateData.cs @@ -0,0 +1,17 @@ +// 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.Collections.Generic; + +namespace BasicTestApp.AuthTest +{ + // DTO shared between server and client + public class ClientSideAuthenticationStateData + { + public bool IsAuthenticated { get; set; } + + public string UserName { get; set; } + + public Dictionary ExposedClaims { get; set; } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor b/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor new file mode 100644 index 0000000000..70f524a763 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/Links.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Components.Routing + + +

To change the underlying authentication state, go here.

diff --git a/src/Components/test/testassets/BasicTestApp/AuthTest/ServerAuthenticationStateProvider.cs b/src/Components/test/testassets/BasicTestApp/AuthTest/ServerAuthenticationStateProvider.cs new file mode 100644 index 0000000000..08639f9254 --- /dev/null +++ b/src/Components/test/testassets/BasicTestApp/AuthTest/ServerAuthenticationStateProvider.cs @@ -0,0 +1,43 @@ +// 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.Linq; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; + +namespace BasicTestApp.AuthTest +{ + // This is intended to be similar to the authentication stateprovider included by default + // with the client-side Blazor "Hosted in ASP.NET Core" template + public class ServerAuthenticationStateProvider : AuthenticationStateProvider + { + private readonly HttpClient _httpClient; + + public ServerAuthenticationStateProvider(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public override async Task GetAuthenticationStateAsync() + { + var uri = new Uri(_httpClient.BaseAddress, "/api/User"); + var data = await _httpClient.GetJsonAsync(uri.AbsoluteUri); + ClaimsIdentity identity; + if (data.IsAuthenticated) + { + var claims = new[] { new Claim(ClaimTypes.Name, data.UserName) } + .Concat(data.ExposedClaims.Select(c => new Claim(c.Key, c.Value))); + identity = new ClaimsIdentity(claims, "Server authentication"); + } + else + { + identity = new ClaimsIdentity(); + } + + return new AuthenticationState(new ClaimsPrincipal(identity)); + } + } +} diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor index 7348a5626f..7570d92e99 100644 --- a/src/Components/test/testassets/BasicTestApp/Index.razor +++ b/src/Components/test/testassets/BasicTestApp/Index.razor @@ -54,6 +54,8 @@ + + @if (SelectedComponentType != null) diff --git a/src/Components/test/testassets/BasicTestApp/Startup.cs b/src/Components/test/testassets/BasicTestApp/Startup.cs index 0f509b98e8..73084e0260 100644 --- a/src/Components/test/testassets/BasicTestApp/Startup.cs +++ b/src/Components/test/testassets/BasicTestApp/Startup.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Runtime.InteropServices; +using BasicTestApp.AuthTest; using Microsoft.AspNetCore.Blazor.Http; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Builder; using Microsoft.Extensions.DependencyInjection; @@ -12,6 +14,7 @@ namespace BasicTestApp { public void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); } public void Configure(IComponentsApplicationBuilder app) diff --git a/src/Components/test/testassets/TestServer/Components.TestServer.csproj b/src/Components/test/testassets/TestServer/Components.TestServer.csproj index 3566f065b3..bf0a093d11 100644 --- a/src/Components/test/testassets/TestServer/Components.TestServer.csproj +++ b/src/Components/test/testassets/TestServer/Components.TestServer.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.0 @@ -6,6 +6,7 @@ + diff --git a/src/Components/test/testassets/TestServer/Controllers/UserController.cs b/src/Components/test/testassets/TestServer/Controllers/UserController.cs new file mode 100644 index 0000000000..16764e4080 --- /dev/null +++ b/src/Components/test/testassets/TestServer/Controllers/UserController.cs @@ -0,0 +1,28 @@ +using System.Linq; +using BasicTestApp.AuthTest; +using Microsoft.AspNetCore.Mvc; + +namespace Components.TestServer.Controllers +{ + [Route("api/[controller]")] + public class UserController : Controller + { + // GET api/user + [HttpGet] + public ClientSideAuthenticationStateData Get() + { + // Servers are not expected to expose everything from the server-side ClaimsPrincipal + // to the client. It's up to the developer to choose what kind of authentication state + // data is needed on the client so it can display suitable options in the UI. + + return new ClientSideAuthenticationStateData + { + IsAuthenticated = User.Identity.IsAuthenticated, + UserName = User.Identity.Name, + ExposedClaims = User.Claims + .Where(c => c.Type == "test-claim") + .ToDictionary(c => c.Type, c => c.Value) + }; + } + } +} diff --git a/src/Components/test/testassets/TestServer/Pages/Authentication.cshtml b/src/Components/test/testassets/TestServer/Pages/Authentication.cshtml new file mode 100644 index 0000000000..3f6cbdefd5 --- /dev/null +++ b/src/Components/test/testassets/TestServer/Pages/Authentication.cshtml @@ -0,0 +1,72 @@ +@page +@using Microsoft.AspNetCore.Authentication +@using System.Security.Claims + + + + Authentication + + +

Authentication

+

+ This is a completely fake login mechanism for automated test purposes. + It accepts any username, with no password. +

+

+ Obviously you should not do this in real applications. + See also: documentation on configuring a real login system. +

+ +
+

Sign in

+ + @* Do not use method="get" for real login forms. This is just to simplify E2E tests. *@ +
+

+ User name: + +

+

+ +

+
+
+ +
+

Status

+

+ Authenticated: @User.Identity.IsAuthenticated + Username: @User.Identity.Name +

+ Sign out +
+ + + +@functions { + public async Task OnGet() + { + if (Request.Query["signout"] == "true") + { + await HttpContext.SignOutAsync(); + return Redirect("Authentication"); + } + + var username = Request.Query["username"]; + if (!string.IsNullOrEmpty(username)) + { + var claims = new List + { + new Claim(ClaimTypes.Name, username), + new Claim("test-claim", "Test claim value"), + }; + + await HttpContext.SignInAsync( + new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType"))); + + return Redirect("Authentication"); + } + + return Page(); + } +} diff --git a/src/Components/test/testassets/TestServer/Startup.cs b/src/Components/test/testassets/TestServer/Startup.cs index cf7ea69d32..26da1af8e0 100644 --- a/src/Components/test/testassets/TestServer/Startup.cs +++ b/src/Components/test/testassets/TestServer/Startup.cs @@ -1,4 +1,5 @@ using BasicTestApp; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Components.Server; using Microsoft.AspNetCore.Hosting; @@ -28,6 +29,7 @@ namespace TestServer options.AddPolicy("AllowAll", _ => { /* Controlled below */ }); }); services.AddServerSideBlazor(); + services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -50,6 +52,7 @@ namespace TestServer .AllowCredentials(); }); + app.UseAuthentication(); // Mount the server-side Blazor app on /subdir app.Map("/subdir", subdirApp => @@ -70,6 +73,7 @@ namespace TestServer app.UseEndpoints(endpoints => { endpoints.MapControllers(); + endpoints.MapRazorPages(); }); // Separately, mount a prerendered server-side Blazor app on /prerendered