Skip to content

Commit 9e015b7

Browse files
authored
[Blazor] Fix antiforgery not being available after first render (#57237)
We make sure to read the antiforgery token from persistent component state when any of the interactive runtimes start.
1 parent 41eebde commit 9e015b7

File tree

8 files changed

+173
-23
lines changed

8 files changed

+173
-23
lines changed

src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,27 +21,4 @@
2121
<Reference Include="Microsoft.AspNetCore.Mvc" />
2222
</ItemGroup>
2323

24-
<Target Name="FixDevelopmentManifest" AfterTargets="GenerateStaticWebAssetsManifest">
25-
<ComputeStaticWebAssetsTargetPaths
26-
Assets="@(StaticWebAsset)"
27-
PathPrefix=""
28-
UseAlternatePathDirectorySeparator="true">
29-
<Output TaskParameter="AssetsWithTargetPath" ItemName="_FixedAssets" />
30-
</ComputeStaticWebAssetsTargetPaths>
31-
32-
<ItemGroup>
33-
<_FixedAssets>
34-
<RelativePath>$([System.String]::Copy('%(_FixedAssets.TargetPath)').Replace('%(_FixedAssets.BasePath)', ''))</RelativePath>
35-
</_FixedAssets>
36-
</ItemGroup>
37-
38-
<GenerateStaticWebAssetsDevelopmentManifest
39-
DiscoveryPatterns="@(StaticWebAssetDiscoveryPattern)"
40-
Assets="@(_FixedAssets)"
41-
Source="$(PackageId)"
42-
ManifestPath="$(StaticWebAssetDevelopmentManifestPath)">
43-
</GenerateStaticWebAssetsDevelopmentManifest>
44-
45-
</Target>
46-
4724
</Project>

src/Components/Server/src/Circuits/CircuitFactory.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Linq;
55
using System.Security.Claims;
6+
using Microsoft.AspNetCore.Components.Forms;
67
using Microsoft.AspNetCore.Components.Infrastructure;
78
using Microsoft.AspNetCore.Components.Routing;
89
using Microsoft.Extensions.DependencyInjection;
@@ -69,6 +70,7 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(
6970
// when the first set of components is provided via an UpdateRootComponents call.
7071
var appLifetime = scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
7172
await appLifetime.RestoreStateAsync(store);
73+
RestoreAntiforgeryToken(scope);
7274
}
7375

7476
var serverComponentDeserializer = scope.ServiceProvider.GetRequiredService<IServerComponentDeserializer>();
@@ -112,6 +114,15 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(
112114
return circuitHost;
113115
}
114116

117+
private static void RestoreAntiforgeryToken(AsyncServiceScope scope)
118+
{
119+
// GetAntiforgeryToken makes sure the antiforgery token is restored from persitent component
120+
// state and is available on the circuit whether or not is used by a component on the first
121+
// render.
122+
var antiforgery = scope.ServiceProvider.GetService<AntiforgeryStateProvider>();
123+
_ = antiforgery?.GetAntiforgeryToken();
124+
}
125+
115126
private static partial class Log
116127
{
117128
[LoggerMessage(1, LogLevel.Debug, "Created circuit {CircuitId} for connection {ConnectionId}", EventName = "CreatedCircuit")]

src/Components/Server/src/Circuits/CircuitHost.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Security.Claims;
77
using Microsoft.AspNetCore.Components.Authorization;
8+
using Microsoft.AspNetCore.Components.Forms;
89
using Microsoft.AspNetCore.Components.Infrastructure;
910
using Microsoft.AspNetCore.SignalR;
1011
using Microsoft.Extensions.DependencyInjection;
@@ -758,6 +759,7 @@ internal Task UpdateRootComponents(
758759
// provided during the start up process
759760
var appLifetime = _scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
760761
await appLifetime.RestoreStateAsync(store);
762+
RestoreAntiforgeryToken(_scope);
761763
}
762764

763765
// Retrieve the circuit handlers at this point.
@@ -802,6 +804,15 @@ internal Task UpdateRootComponents(
802804
});
803805
}
804806

807+
private static void RestoreAntiforgeryToken(AsyncServiceScope scope)
808+
{
809+
// GetAntiforgeryToken makes sure the antiforgery token is restored from persitent component
810+
// state and is available on the circuit whether or not is used by a component on the first
811+
// render.
812+
var antiforgery = scope.ServiceProvider.GetService<AntiforgeryStateProvider>();
813+
_ = antiforgery?.GetAntiforgeryToken();
814+
}
815+
805816
private async ValueTask PerformRootComponentOperations(
806817
RootComponentOperation[] operations,
807818
bool shouldWaitForQuiescence)

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHost.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Reflection.Metadata;
6+
using Microsoft.AspNetCore.Components.Forms;
67
using Microsoft.AspNetCore.Components.Infrastructure;
78
using Microsoft.AspNetCore.Components.Web.Infrastructure;
89
using Microsoft.AspNetCore.Components.WebAssembly.HotReload;
@@ -137,6 +138,8 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl
137138

138139
await manager.RestoreStateAsync(store);
139140

141+
RestoreAntiforgeryToken();
142+
140143
if (MetadataUpdater.IsSupported)
141144
{
142145
await WebAssemblyHotReload.InitializeAsync();
@@ -230,4 +233,11 @@ private static void AddWebRootComponents(WebAssemblyRenderer renderer, RootCompo
230233

231234
renderer.NotifyEndUpdateRootComponents(operationBatch.BatchId);
232235
}
236+
237+
private void RestoreAntiforgeryToken()
238+
{
239+
// The act of instantiating the DefaultAntiforgeryStateProvider will automatically
240+
// retrieve the antiforgery token from the persistent state
241+
_scope.ServiceProvider.GetRequiredService<AntiforgeryStateProvider>();
242+
}
233243
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Components.TestServer.RazorComponents;
10+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
11+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
12+
using Microsoft.AspNetCore.E2ETesting;
13+
using OpenQA.Selenium;
14+
using TestServer;
15+
using Xunit.Abstractions;
16+
17+
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests.FormHandlingTests;
18+
19+
public class AntiforgeryTests : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
20+
{
21+
public AntiforgeryTests(
22+
BrowserFixture browserFixture,
23+
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
24+
ITestOutputHelper output)
25+
: base(browserFixture, serverFixture, output)
26+
{
27+
}
28+
29+
public override Task InitializeAsync()
30+
=> InitializeAsync(BrowserFixture.StreamingContext);
31+
32+
[Theory]
33+
[InlineData("server")]
34+
[InlineData("webassembly")]
35+
public void CanUseAntiforgeryAfterInitialRender(string target)
36+
{
37+
Navigate($"{ServerPathBase}/{target}-antiforgery-form");
38+
39+
Browser.Exists(By.Id("interactive"));
40+
41+
Browser.Click(By.Id("render-form"));
42+
43+
var input = Browser.Exists(By.Id("name"));
44+
input.SendKeys("Test");
45+
var submit = Browser.Exists(By.Id("submit"));
46+
submit.Click();
47+
48+
var result = Browser.Exists(By.Id("result"));
49+
Browser.Equal("Test", () => result.Text);
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@page "/server-antiforgery-form/{RenderForm=false}"
2+
@using Microsoft.AspNetCore.Components.Web
3+
@rendermode RenderMode.InteractiveServer
4+
5+
@if (string.IsNullOrEmpty(Name))
6+
{
7+
<TestContentPackage.InteractiveAntiforgery RenderForm="bool.Parse(RenderForm)" />
8+
}
9+
else
10+
{
11+
<p id="result">@Name</p>
12+
}
13+
14+
@code {
15+
[Parameter] public string RenderForm { get; set; } = null!;
16+
17+
[SupplyParameterFromQuery] public string Name { get; set; } = "";
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@page "/webassembly-antiforgery-form/{RenderForm=false}"
2+
@using Microsoft.AspNetCore.Components.Web
3+
@rendermode RenderMode.InteractiveWebAssembly
4+
5+
@if (string.IsNullOrEmpty(Name))
6+
{
7+
<TestContentPackage.InteractiveAntiforgery RenderForm="@bool.Parse(RenderForm)" />
8+
}
9+
else
10+
{
11+
<p id="result">@Name</p>
12+
}
13+
14+
@code {
15+
[SupplyParameterFromQuery] public string Name { get; set; } = "";
16+
17+
[Parameter] public string RenderForm { get; set; } = null!;
18+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
@using Microsoft.AspNetCore.Components.Forms
2+
3+
@if (RenderForm)
4+
{
5+
if (RendererInfo.IsInteractive)
6+
{
7+
<p id="interactive">Interactive</p>
8+
9+
<form @formname="Sample" method="post">
10+
<label for="name">Name:</label>
11+
<input type="text" id="name" name="name" />
12+
<AntiforgeryToken />
13+
<input type="hidden" name="_handler" value="Sample" />
14+
<button id="submit" type="submit">Submit</button>
15+
</form>
16+
}
17+
else
18+
{
19+
<form @formname="Sample" method="post" @onsubmit="Redirect">
20+
<label for="name">Name:</label>
21+
<input type="text" id="name" name="name" />
22+
<AntiforgeryToken />
23+
<button type="submit">Submit</button>
24+
</form>
25+
}
26+
}else{
27+
if (RendererInfo.IsInteractive)
28+
{
29+
<p id="interactive">Interactive</p>
30+
}
31+
<a id="render-form" href="@(Navigation.Uri + "/true")">Render form</a>
32+
}
33+
34+
@code {
35+
[SupplyParameterFromForm(FormName = "Sample")] public string Name { get; set; }
36+
37+
[Parameter] public bool RenderForm { get; set; }
38+
39+
[Inject] NavigationManager Navigation { get; set; }
40+
41+
protected override void OnInitialized()
42+
{
43+
Name ??= "";
44+
}
45+
46+
public void Redirect()
47+
{
48+
if (!string.IsNullOrEmpty(Name))
49+
{
50+
var url = Navigation.GetUriWithQueryParameter("Name", Name);
51+
Navigation.NavigateTo(url, forceLoad: true);
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)