Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Blazor] Fix antiforgery not being available after first render #57237

Merged
merged 5 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions src/Components/Samples/BlazorUnitedApp/BlazorUnitedApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,4 @@
<Reference Include="Microsoft.AspNetCore.Mvc" />
</ItemGroup>

<Target Name="FixDevelopmentManifest" AfterTargets="GenerateStaticWebAssetsManifest">
<ComputeStaticWebAssetsTargetPaths
Assets="@(StaticWebAsset)"
PathPrefix=""
UseAlternatePathDirectorySeparator="true">
<Output TaskParameter="AssetsWithTargetPath" ItemName="_FixedAssets" />
</ComputeStaticWebAssetsTargetPaths>

<ItemGroup>
<_FixedAssets>
<RelativePath>$([System.String]::Copy('%(_FixedAssets.TargetPath)').Replace('%(_FixedAssets.BasePath)', ''))</RelativePath>
</_FixedAssets>
</ItemGroup>

<GenerateStaticWebAssetsDevelopmentManifest
DiscoveryPatterns="@(StaticWebAssetDiscoveryPattern)"
Assets="@(_FixedAssets)"
Source="$(PackageId)"
ManifestPath="$(StaticWebAssetDevelopmentManifestPath)">
</GenerateStaticWebAssetsDevelopmentManifest>

</Target>

</Project>
11 changes: 11 additions & 0 deletions src/Components/Server/src/Circuits/CircuitFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -69,6 +70,7 @@ public async ValueTask<CircuitHost> CreateCircuitHostAsync(
// when the first set of components is provided via an UpdateRootComponents call.
var appLifetime = scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
await appLifetime.RestoreStateAsync(store);
RestoreAntiforgeryToken(scope);
}

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

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<AntiforgeryStateProvider>();
_ = antiforgery?.GetAntiforgeryToken();
}

private static partial class Log
{
[LoggerMessage(1, LogLevel.Debug, "Created circuit {CircuitId} for connection {ConnectionId}", EventName = "CreatedCircuit")]
Expand Down
11 changes: 11 additions & 0 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -758,6 +759,7 @@ internal Task UpdateRootComponents(
// provided during the start up process
var appLifetime = _scope.ServiceProvider.GetRequiredService<ComponentStatePersistenceManager>();
await appLifetime.RestoreStateAsync(store);
RestoreAntiforgeryToken(_scope);
}

// Retrieve the circuit handlers at this point.
Expand Down Expand Up @@ -802,6 +804,15 @@ 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<AntiforgeryStateProvider>();
_ = antiforgery?.GetAntiforgeryToken();
}

private async ValueTask PerformRootComponentOperations(
RootComponentOperation[] operations,
bool shouldWaitForQuiescence)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -137,6 +138,8 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl

await manager.RestoreStateAsync(store);

RestoreAntiforgeryToken();

if (MetadataUpdater.IsSupported)
{
await WebAssemblyHotReload.InitializeAsync();
Expand Down Expand Up @@ -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
// retrieve the antiforgery token from the persistent state
_scope.ServiceProvider.GetRequiredService<AntiforgeryStateProvider>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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;
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<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
{
public AntiforgeryTests(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/server-antiforgery-form/{RenderForm=false}"
@using Microsoft.AspNetCore.Components.Web
@rendermode RenderMode.InteractiveServer

@if (string.IsNullOrEmpty(Name))
{
<TestContentPackage.InteractiveAntiforgery RenderForm="bool.Parse(RenderForm)" />
}
else
{
<p id="result">@Name</p>
}

@code {
[Parameter] public string RenderForm { get; set; } = null!;

[SupplyParameterFromQuery] public string Name { get; set; } = "";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/webassembly-antiforgery-form/{RenderForm=false}"
@using Microsoft.AspNetCore.Components.Web
@rendermode RenderMode.InteractiveWebAssembly

@if (string.IsNullOrEmpty(Name))
{
<TestContentPackage.InteractiveAntiforgery RenderForm="@bool.Parse(RenderForm)" />
}
else
{
<p id="result">@Name</p>
}

@code {
[SupplyParameterFromQuery] public string Name { get; set; } = "";

[Parameter] public string RenderForm { get; set; } = null!;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@using Microsoft.AspNetCore.Components.Forms

@if (RenderForm)
{
if (RendererInfo.IsInteractive)
{
<p id="interactive">Interactive</p>

<form @formname="Sample" method="post">
<label for="name">Name:</label>
<input type="text" id="name" name="name" />
<AntiforgeryToken />
<input type="hidden" name="_handler" value="Sample" />
<button id="submit" type="submit">Submit</button>
</form>
}
else
{
<form @formname="Sample" method="post" @onsubmit="Redirect">
<label for="name">Name:</label>
<input type="text" id="name" name="name" />
<AntiforgeryToken />
<button type="submit">Submit</button>
</form>
}
}else{
if (RendererInfo.IsInteractive)
{
<p id="interactive">Interactive</p>
}
<a id="render-form" href="@(Navigation.Uri + "/true")">Render form</a>
}

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