From 31085a24586f37410096b1c23d574f8bbc8409fa Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 8 Aug 2024 20:15:35 +0200 Subject: [PATCH 1/5] Fix antiforgery not being available after first render --- .../BlazorUnitedApp/BlazorUnitedApp.csproj | 23 -------- .../Server/src/Circuits/CircuitFactory.cs | 8 +++ .../Server/src/Circuits/CircuitHost.cs | 8 +++ .../src/Hosting/WebAssemblyHost.cs | 10 ++++ .../FormHandlingTests/AntiforgeryTests.cs | 48 +++++++++++++++++ .../Pages/Forms/ServerAntiforgeryForm.razor | 18 +++++++ .../Pages/WebAssemblyAntiforgeryForm.razor | 18 +++++++ .../InteractiveAntiforgery.razor | 54 +++++++++++++++++++ 8 files changed, 164 insertions(+), 23 deletions(-) create mode 100644 src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs create mode 100644 src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ServerAntiforgeryForm.razor create mode 100644 src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor create mode 100644 src/Components/test/testassets/TestContentPackage/InteractiveAntiforgery.razor diff --git a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj index 57add88b29ff..62460261002b 100644 --- a/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj +++ b/src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj @@ -21,27 +21,4 @@ - - - - - - - <_FixedAssets> - $([System.String]::Copy('%(_FixedAssets.TargetPath)').Replace('%(_FixedAssets.BasePath)', '')) - - - - - - - - diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index c22aca391664..7c61455344df 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Security.Claims; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Routing; using Microsoft.Extensions.DependencyInjection; @@ -69,6 +70,7 @@ public async ValueTask CreateCircuitHostAsync( // when the first set of components is provided via an UpdateRootComponents call. var appLifetime = scope.ServiceProvider.GetRequiredService(); await appLifetime.RestoreStateAsync(store); + RestoreAntiforgeryToken(scope); } var serverComponentDeserializer = scope.ServiceProvider.GetRequiredService(); @@ -112,6 +114,12 @@ public async ValueTask CreateCircuitHostAsync( return circuitHost; } + private static void RestoreAntiforgeryToken(AsyncServiceScope scope) + { + var antiforgery = scope.ServiceProvider.GetService(); + _ = antiforgery?.GetAntiforgeryToken(); + } + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Created circuit {CircuitId} for connection {ConnectionId}", EventName = "CreatedCircuit")] diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index 7acea8c63fa3..d3f401741bb5 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; @@ -758,6 +759,7 @@ internal Task UpdateRootComponents( // provided during the start up process var appLifetime = _scope.ServiceProvider.GetRequiredService(); await appLifetime.RestoreStateAsync(store); + RestoreAntiforgeryToken(_scope); } // Retrieve the circuit handlers at this point. @@ -802,6 +804,12 @@ internal Task UpdateRootComponents( }); } + private static void RestoreAntiforgeryToken(AsyncServiceScope scope) + { + var antiforgery = scope.ServiceProvider.GetService(); + _ = antiforgery?.GetAntiforgeryToken(); + } + private async ValueTask PerformRootComponentOperations( RootComponentOperation[] operations, bool shouldWaitForQuiescence) diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index 32cabdf6c564..0bf05486e9f8 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata; +using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.HotReload; @@ -137,6 +138,8 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl await manager.RestoreStateAsync(store); + RestoreAntiforgeryToken(); + if (MetadataUpdater.IsSupported) { await WebAssemblyHotReload.InitializeAsync(); @@ -230,4 +233,11 @@ private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootCompo renderer.NotifyEndUpdateRootComponents(operationBatch.BatchId); } + + private void RestoreAntiforgeryToken() + { + // The act of instantiating the DefaultAntiforgeryStateProvider will automatically + // will retrieve the antiforgery token from the persistent state + _scope.ServiceProvider.GetRequiredService(); + } } diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs new file mode 100644 index 000000000000..453562510ad8 --- /dev/null +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Components.TestServer.RazorComponents; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; +using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; +using Microsoft.AspNetCore.E2ETesting; +using OpenQA.Selenium; +using TestServer; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.FormHandlingTests; + +public class AntiforgeryTests : ServerTestBase>> +{ + public AntiforgeryTests( + BrowserFixture browserFixture, + BasicTestAppServerSiteFixture> serverFixture, + ITestOutputHelper output) + : base(browserFixture, serverFixture, output) + { + } + + public override Task InitializeAsync() + => InitializeAsync(BrowserFixture.StreamingContext); + + [Theory] + [InlineData("server")] + [InlineData("webassembly")] + public void CanUseAntiforgeryAfterInitialRender(string target) + { + Navigate($"{ServerPathBase}/{target}-antiforgery-form"); + + Browser.Exists(By.Id("interactive")); + + Browser.Click(By.Id("render-form")); + + var input = Browser.Exists(By.Id("name")); + input.SendKeys("Test"); + var submit = Browser.Exists(By.Id("submit")); + submit.Click(); + + var result = Browser.Exists(By.Id("result")); + Browser.Equal("Test", () => result.Text); + } +} diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ServerAntiforgeryForm.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ServerAntiforgeryForm.razor new file mode 100644 index 000000000000..a2c3eacc0e87 --- /dev/null +++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/ServerAntiforgeryForm.razor @@ -0,0 +1,18 @@ +@page "/server-antiforgery-form/{RenderForm=false}" +@using Microsoft.AspNetCore.Components.Web +@rendermode RenderMode.InteractiveServer + +@if (string.IsNullOrEmpty(Name)) +{ + +} +else +{ +

@Name

+} + +@code { + [Parameter] public string RenderForm { get; set; } = null!; + + [SupplyParameterFromQuery] public string Name { get; set; } = ""; +} diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor new file mode 100644 index 000000000000..509c3bd4b0a4 --- /dev/null +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor @@ -0,0 +1,18 @@ +@page "/webassembly-antiforgery-form/{RenderForm=false}" +@using Microsoft.AspNetCore.Components.Web +@rendermode RenderMode.InteractiveWebAssembly + +@if (string.IsNullOrEmpty(Name)) +{ + +} +else +{ +

@Name

+} + +@code { + [SupplyParameterFromQuery] public string Name { get; set; } = ""; + + [Parameter] public string RenderForm { get; set; } +} diff --git a/src/Components/test/testassets/TestContentPackage/InteractiveAntiforgery.razor b/src/Components/test/testassets/TestContentPackage/InteractiveAntiforgery.razor new file mode 100644 index 000000000000..84d4a5824119 --- /dev/null +++ b/src/Components/test/testassets/TestContentPackage/InteractiveAntiforgery.razor @@ -0,0 +1,54 @@ +@using Microsoft.AspNetCore.Components.Forms + +@if (RenderForm) +{ + if (RendererInfo.IsInteractive) + { +

Interactive

+ +
+ + + + + + + } + else + { +
+ + + + + + } +}else{ + if (RendererInfo.IsInteractive) + { +

Interactive

+ } + Render form +} + +@code { + [SupplyParameterFromForm(FormName = "Sample")] public string Name { get; set; } + + [Parameter] public bool RenderForm { get; set; } + + [Inject] NavigationManager Navigation { get; set; } + + protected override void OnInitialized() + { + Name ??= ""; + } + + public void Redirect() + { + if (!string.IsNullOrEmpty(Name)) + { + var url = Navigation.GetUriWithQueryParameter("Name", Name); + Navigation.NavigateTo(url, forceLoad: true); + } + } +} From b4160fe3e8b9c6ee4fe9df11b65a18d691be0863 Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 8 Aug 2024 20:44:39 +0200 Subject: [PATCH 2/5] Update src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs Co-authored-by: Mackinnon Buck --- .../WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs index 0bf05486e9f8..a36bb957ccce 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs @@ -237,7 +237,7 @@ private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootCompo private void RestoreAntiforgeryToken() { // The act of instantiating the DefaultAntiforgeryStateProvider will automatically - // will retrieve the antiforgery token from the persistent state + // retrieve the antiforgery token from the persistent state _scope.ServiceProvider.GetRequiredService(); } } From 914290b6853cf463584c03cab4ef761e1f31d28e Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Thu, 8 Aug 2024 20:48:52 +0200 Subject: [PATCH 3/5] More fixes --- src/Components/Server/src/Circuits/CircuitFactory.cs | 3 +++ src/Components/Server/src/Circuits/CircuitHost.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Components/Server/src/Circuits/CircuitFactory.cs b/src/Components/Server/src/Circuits/CircuitFactory.cs index 7c61455344df..92526e87ad3c 100644 --- a/src/Components/Server/src/Circuits/CircuitFactory.cs +++ b/src/Components/Server/src/Circuits/CircuitFactory.cs @@ -116,6 +116,9 @@ public async ValueTask CreateCircuitHostAsync( private static void RestoreAntiforgeryToken(AsyncServiceScope scope) { + // GetAntiforgeryToken makes sure the antiforgery token is restored from persitent component + // state and is available on the circuit whether or not is used by a component on the first + // render. var antiforgery = scope.ServiceProvider.GetService(); _ = antiforgery?.GetAntiforgeryToken(); } diff --git a/src/Components/Server/src/Circuits/CircuitHost.cs b/src/Components/Server/src/Circuits/CircuitHost.cs index d3f401741bb5..f1983d987a58 100644 --- a/src/Components/Server/src/Circuits/CircuitHost.cs +++ b/src/Components/Server/src/Circuits/CircuitHost.cs @@ -806,6 +806,9 @@ internal Task UpdateRootComponents( private static void RestoreAntiforgeryToken(AsyncServiceScope scope) { + // GetAntiforgeryToken makes sure the antiforgery token is restored from persitent component + // state and is available on the circuit whether or not is used by a component on the first + // render. var antiforgery = scope.ServiceProvider.GetService(); _ = antiforgery?.GetAntiforgeryToken(); } From 5d1dbd75029cc9a83246eb88012881d162d746cd Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 9 Aug 2024 12:44:14 +0200 Subject: [PATCH 4/5] Fix build break --- .../Pages/WebAssemblyAntiforgeryForm.razor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor b/src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor index 509c3bd4b0a4..657f40f3cc44 100644 --- a/src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor +++ b/src/Components/test/testassets/Components.WasmMinimal/Pages/WebAssemblyAntiforgeryForm.razor @@ -14,5 +14,5 @@ else @code { [SupplyParameterFromQuery] public string Name { get; set; } = ""; - [Parameter] public string RenderForm { get; set; } + [Parameter] public string RenderForm { get; set; } = null!; } From ea5989df2e6ff6c36d3ccee7c550e99a090202dd Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Fri, 9 Aug 2024 13:14:30 +0200 Subject: [PATCH 5/5] Add missing license header --- .../ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs index 453562510ad8..12defa51dc5a 100644 --- a/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs +++ b/src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/AntiforgeryTests.cs @@ -1,3 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + using System; using System.Collections.Generic; using System.Linq;