Skip to content

Commit

Permalink
[Blazor] Antiforgery fix (#50946)
Browse files Browse the repository at this point in the history
# Ensure antiforgery token flows to Blazor WebAssembly

The change makes sure that we persist the Antiforgery token during prerendering so that it is available to WebAssembly components.

## Description

* When using cookie authentication it is necessary to use antiforgery protection to prevent cross-site request forgery attacks.
* Blazor Webassembly interactive components must get access to the request antiforgery token to attach it to any outgoing API call.
* The antiforgery request token was not flowing from the server to the client correctly.
* This change enables calling APIs from web assembly to the server safely.
Fixes #50900

## Customer Impact

.NET 8.0 customers who have created Blazor Web Apps will fail to call APIs from webassembly components, as they won't be able to attach the required antiforgery token.

## Regression?

- [ ] Yes
- [X] No

[If yes, specify the version the behavior has regressed from]

## Risk

- [ ] High
- [ ] Medium
- [X] Low

The fix is simple and we added an E2E test to cover the scenario.

## Verification

- [ ] Manual (required)
- [X] Automated

## Packaging changes reviewed?

- [ ] Yes
- [ ] No
- [X] N/A

----

## When servicing release/2.1

- [ ] Make necessary changes in eng/PatchConfig.props
  • Loading branch information
javiercn authored Sep 27, 2023
1 parent 0cbe08f commit 8b27376
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 2 deletions.
4 changes: 2 additions & 2 deletions src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public DefaultAntiforgeryStateProvider(PersistentComponentState state)
// don't have access to the request.
_subscription = state.RegisterOnPersisting(() =>
{
state.PersistAsJson(PersistenceKey, _currentToken);
state.PersistAsJson(PersistenceKey, GetAntiforgeryToken());
return Task.CompletedTask;
}, new InteractiveAutoRenderMode());
}, RenderMode.InteractiveWebAssembly);

state.TryTakeFromJson(PersistenceKey, out _currentToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,19 @@ public void FormNoAntiforgeryReturnBadRequest(bool suppressEnhancedNavigation)
DispatchToFormCore(dispatchToForm);
}

[Fact]
public void CanUseAntiforgeryTokenInWasm()
{
var dispatchToForm = new DispatchToForm(this)
{
Url = "forms/antiforgery-wasm",
FormCssSelector = "form",
InputFieldId = "Value",
SuppressEnhancedNavigation = true,
};
DispatchToFormCore(dispatchToForm);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Http.Results" />
<Reference Include="Microsoft.AspNetCore.Cors" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Components.TestServer.RazorComponents.Pages.Forms;
using Components.TestServer.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Server;
using Microsoft.AspNetCore.Mvc;

namespace TestServer;

Expand Down Expand Up @@ -154,6 +155,11 @@ private static void MapEnhancedNavigationEndpoints(IEndpointRouteBuilder endpoin
await response.WriteAsync("<html><body><h1>This is a non-Blazor endpoint</h1><p>That's all</p></body></html>");
});

endpoints.MapPost("api/antiforgery-form", ([FromForm] string value) =>
{
return Results.Ok(value);
});

static Task PerformRedirection(HttpRequest request, HttpResponse response)
{
response.Redirect(request.Query["external"] == "true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@page "/forms/antiforgery-wasm"
<h3>FormRunningOnWasmCanUseAntiforgeryToken</h3>

<TestContentPackage.WasmFormComponent/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
@attribute [RenderModeInteractiveWebAssembly]
@using Microsoft.AspNetCore.Components.Forms
@using System.Net.Http
<h3>WasmFormComponent</h3>

<form @formname="WasmForm" @onsubmit="SubmitForm">
<input id="Value" @bind="_value" name="Value" />
@if (OperatingSystem.IsBrowser())
{
<input id="send" type="submit" value="Send" />
}
<AntiforgeryToken />
</form>

@if (_posted)
{
@if (_succeeded)
{
<p id="pass">Posting the value succeded.</p>
}
else
{
<p>Posting the value failed.</p>
}
}
else
{
<p>Antiforgery: @_token</p>
}

@code {
string _value;
string _token;
bool _succeeded;
bool _posted;

[Inject] public AntiforgeryStateProvider AntiforgeryState { get; set; }
[Inject] public NavigationManager Navigation { get; set; }

protected override void OnInitialized()
{
if (OperatingSystem.IsBrowser())
{
var antiforgery = AntiforgeryState.GetAntiforgeryToken();
_token = antiforgery.Value;
}
}

private async Task SubmitForm()
{
if (OperatingSystem.IsBrowser())
{
var client = new HttpClient() { BaseAddress = new Uri(Navigation.BaseUri) };
var antiforgery = AntiforgeryState.GetAntiforgeryToken();
if (antiforgery != null)
{
_posted = true;
var request = new HttpRequestMessage(HttpMethod.Post, "api/antiforgery-form");
var content = new FormUrlEncodedContent(new KeyValuePair<string, string>[] { new ("Value", _value) });
request.Content = content;
request.Headers.Add("RequestVerificationToken", antiforgery.Value);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
_succeeded = true;
}
else
{
_succeeded = false;
}
}
}
}
}

0 comments on commit 8b27376

Please sign in to comment.