Antiforgery and Blazor

4/25/2022

By: Shaun Walker

As I mentioned in my previous Antiforgery post, the basic antiforgery pattern is that the server sends a token associated with the current user's identity to the client, the client sends back the token to the server for verification, and if the server receives a token that doesn't match the authenticated user's identity, the request is rejected.

The key to understanding this security feature is that it is directly linked to the user's identity. This means that the antiforgery token and cookie that are generated by .NET Core are only valid for a specific user running in a specific context. So each time a user's identity changes ( ie.login/logout) new antiforgery tokens and cookies need to be generated for them to remain valid.

In a traditional server-based application this happens as part of the normal page refresh life cycle. However, a Blazor application is a single page application (SPA) with a distinct client and server workload. In this operating model, the client never experiences a full page refresh once the initial page load is complete. Therefore, when a user's identity changes, the antiforgery information in the client can become invalid and needs to be refreshed. The tricky part is how to facilitate this refresh.

It turns out that a simple redirect between the client and server is not effective. In order to generate a new antiforgery ticket and cookie, the redirect flow must be based on a POST operation. And since Blazor does not natively work with traditional HTML form concepts, we need a creative solution. Luckily, we had already tackled this challenge when we originally implemented authentication in Oqtane for Blazor Server.

The solution involves a form submission method written in JavaScript, wrapped in a JS Interop method and then called from the client Blazor application. The target of the form post is a razor page on the server whose sole purpose is to utilize the Return Url form field passed to it and transfer control back to the client. This redirect flow will automatically trigger the antiforgery middleware to refresh the antiforgery token and cookie.

Oqtane.Server\wwwroot\js\interop.js

submitForm: function (path, fields) {
    const form = document.createElement('form');
    form.method = 'post';
    form.action = path;

    for (const key in fields) {
        if (fields.hasOwnProperty(key)) {
            const hiddenField = document.createElement('input');
            hiddenField.type = 'hidden';
            hiddenField.name = key;
            hiddenField.value = fields[key];
            form.appendChild(hiddenField);
        }
    }

    document.body.appendChild(form);
    form.submit();
}

Oqtane.Client\UI\Interop.cs

public Task SubmitForm(string path, object fields)
{
    try
    {
        _jsRuntime.InvokeVoidAsync("Oqtane.Interop.submitForm", path, fields);
        return Task.CompletedTask;
    }
    catch
    {
        return Task.CompletedTask;
    }
}

Oqtane.Client\UI\SiteRouter.razor

if (querystring["reload"] == "post")
{
    // post back so that the cookies are set correctly - required on any change to the principal
    var interop = new Interop(JSRuntime);
    var fields = new { returnurl = "/" + NavigationManager.ToBaseRelativePath(_absoluteUri) };
    string url = Utilities.TenantUrl(SiteState.Alias, "/pages/external/");
    await interop.SubmitForm(url, fields);
    return;
}

Oqtane.Server\Pages\External.cshtml.cs

public IActionResult OnPostAsync(string returnurl)
{
    // remove reload parameter
    returnurl = returnurl.Replace("?reload=post", "");

    return LocalRedirect(Url.Content("~" + returnurl));
}

It is possible that you have not run into this situation in your own Blazor applications. This might be because you do not have antiforgery enabled, you are not using a Backend-for-Frontend architecture, or you are relying on Jwt authorization instead. Or perhaps its is because you are using the default .NET Core Identity where the authentication is all implemented server-side using Razor Pages rather than Blazor components. Either way, you may still run into this issue at some point if you try to integrate with a remote service directly from your Blazor client. A good example is integration with OAuth 2.0 or OpenId Connect services which utilize a callback url to return the user flow to your application. In this scenario, the login may appear to be successful however you will eventually experience strange behavior in the client application because you do not have a valid antiforgery token.


Do You Want To Be Notified When Blogs Are Published?
RSS