Skip to content

Commit

Permalink
Fix XSRF in Blazor Bill Splitter
Browse files Browse the repository at this point in the history
Fixes #8
  • Loading branch information
AustinWise committed Sep 24, 2023
1 parent 90eeb29 commit b8e4eff
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/DkpWeb.Blazor/Client/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Austin.DkpLib;
using DkpWeb.Blazor.Services;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

Expand All @@ -14,6 +15,7 @@ public static async Task Main(string[] args)

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IBillSplitterServices, HttpBillSplitterServices>();
builder.Services.AddSingleton<AntiforgeryStateProvider, AustinAntiforgeryStateProvider>();

await builder.Build().RunAsync();
}
Expand Down
43 changes: 43 additions & 0 deletions src/DkpWeb.Blazor/Services/AustinAntiforgeryStateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// Based on https://github.com/dotnet/aspnetcore/blob/8ad057426fa6a27cd648b05684afddab9d97d3d9/src/Components/Shared/src/DefaultAntiforgeryStateProvider.cs

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System.Diagnostics.CodeAnalysis;

namespace DkpWeb.Blazor.Services
{
public class AustinAntiforgeryStateProvider : AntiforgeryStateProvider, IDisposable
{
private const string PersistenceKey = $"__austin__{nameof(AntiforgeryRequestToken)}";
private readonly PersistingComponentStateSubscription _subscription;
private readonly AntiforgeryRequestToken? _currentToken;

[UnconditionalSuppressMessage(
"Trimming",
"IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
Justification = $"{nameof(AustinAntiforgeryStateProvider)} uses the {nameof(PersistentComponentState)} APIs to deserialize the token, which are already annotated.")]
public AustinAntiforgeryStateProvider(PersistentComponentState state)
{
// Automatically flow the Request token to server/wasm through
// persistent component state. This guarantees that the antiforgery
// token is available on the interactive components, even when they
// don't have access to the request.
_subscription = state.RegisterOnPersisting(() =>
{
state.PersistAsJson(PersistenceKey, GetAntiforgeryToken());
return Task.CompletedTask;
});

state.TryTakeFromJson(PersistenceKey, out _currentToken);
}

/// <inheritdoc />
public override AntiforgeryRequestToken? GetAntiforgeryToken() => _currentToken;

/// <inheritdoc />
public void Dispose() => _subscription.Dispose();
}
}
16 changes: 14 additions & 2 deletions src/DkpWeb.Blazor/Services/HttpBillSplitterServices.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
using Austin.DkpLib;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Options;
using System.Net.Http.Json;

namespace DkpWeb.Blazor.Services;

public sealed class HttpBillSplitterServices : IBillSplitterServices
{
private readonly HttpClient mClient;
private readonly AntiforgeryStateProvider mAntiforgery;

public HttpBillSplitterServices(HttpClient httpClient)
public HttpBillSplitterServices(HttpClient httpClient, AntiforgeryStateProvider antiforgery)
{
this.mClient = httpClient;
this.mAntiforgery = antiforgery;
}

public async Task<SplitPerson[]> GetAllPeopleAsync()
Expand All @@ -19,6 +23,14 @@ public async Task<SplitPerson[]> GetAllPeopleAsync()

public async Task SaveBillSplitResult(BillSplitResult result)
{
await mClient.PostAsJsonAsync("api/BillSplit", result);
var antiforgery = mAntiforgery.GetAntiforgeryToken();
var request = new HttpRequestMessage(HttpMethod.Post, "api/BillSplit");
if (antiforgery is not null)
{
request.Headers.Add("RequestVerificationToken", antiforgery.Value);
}
JsonContent content = JsonContent.Create(result);
request.Content = content;
await mClient.SendAsync(request);
}
}
1 change: 1 addition & 0 deletions src/DkpWeb/Areas/API/BillSplitController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public BillSplitController(IBillSplitterServices data)
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([FromBody] BillSplitResult result)
{
await mData.SaveBillSplitResult(result);
Expand Down
48 changes: 48 additions & 0 deletions src/DkpWeb/Services/AustinEndpointAntiforgeryStateProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// Based on https://github.com/dotnet/aspnetcore/blob/8ad057426fa6a27cd648b05684afddab9d97d3d9/src/Components/Endpoints/src/Forms/EndpointAntiforgeryStateProvider.cs

using DkpWeb.Blazor.Services;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Http;

namespace DkpWeb.Services
{
public class AustinEndpointAntiforgeryStateProvider : AustinAntiforgeryStateProvider
{
private readonly IAntiforgery antiforgery;
private readonly IHttpContextAccessor accessor;

public AustinEndpointAntiforgeryStateProvider(IAntiforgery antiforgery, PersistentComponentState state, IHttpContextAccessor accessor)
: base(state)
{
this.antiforgery = antiforgery;
this.accessor = accessor;
}

public override AntiforgeryRequestToken GetAntiforgeryToken()
{
var context = accessor.HttpContext;
if (context == null)
{
return null;
}

// We already have a callback setup to generate the token when the response starts if needed.
// If we need the tokens before we start streaming the response, we'll generate and store them;
// otherwise we'll just retrieve them.
// In case there are no tokens available, we are going to return null and no-op.
var tokens = !context.Response.HasStarted ? antiforgery.GetAndStoreTokens(context) : antiforgery.GetTokens(context);
if (tokens.RequestToken is null)
{
return null;
}

return new AntiforgeryRequestToken(tokens.RequestToken, tokens.FormFieldName);
}

}
}
3 changes: 3 additions & 0 deletions src/DkpWeb/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Google.Api;
using Google.Cloud.Diagnostics.AspNetCore3;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpOverrides;
Expand All @@ -30,6 +31,8 @@ public Startup(IConfiguration configuration)

public void ConfigureServices(IServiceCollection services, IWebHostEnvironment env)
{
services.AddScoped<AntiforgeryStateProvider, AustinEndpointAntiforgeryStateProvider>();

services.AddOptions();
services.Configure<EmailOptions>(Configuration.GetSection("Gmail"));

Expand Down

0 comments on commit b8e4eff

Please sign in to comment.