Odolix
Odolix

Reputation: 21

POST data to blazor component

Is it possible for an external site to POST to a Blazor Component (.razor)?

When the client is on my apps' shopping cart page and clicks on payment, he is redirected to an external site handling the payment. The external site then sends back a post request to my Blazor (.razor) component, to the url as specified in my component: @page "/mysite/success". This POST request fails status code 400.

If I add IgnoreAntiforgeryToken to my host page (RAZOR Page .cshtml) then I can post to my blazor component.

@page "/_Host"
@namespace Client

@attribute [IgnoreAntiforgeryToken]

@{
    Layout = "_Layout";
}

@model HostModel

<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />

But I do not want to disable CSRF for all the components. Only for the one component.

Thanks, Louise

Upvotes: 2

Views: 4817

Answers (1)

clamchoda
clamchoda

Reputation: 4981

Yes, it is. The trick to accepting the post is to include IgnoreAntiforgeryToken in the _Host page. The trick to reading the data is to catch the OnPost in the PageModel. The downfall is that Blazor is a single page application and the ignore attribute will be turned on globally.

I can't find an effective way to programmatically add the IgnoreAntiforgeryToken or change the PageModel for an individual page component. Instead I create a list of PostComponentPages that will accept post data.

Hopefully someone can build on this answer and find a better way to turn on IgnoreAntiforgeryToken for an individual component.

Note: In my example I only accept post data to the Counter page.

_Host.cshtml

@{
    Layout = "_Layout";
    @model HostPageModel;

}

HostPageModel.cs

// Required for accepting post data from 3rd party resource.
[IgnoreAntiforgeryToken]
// Not required, but gives us access to the post data and exception handling per request.
public class HostPageModel : PageModel
{
    /// <summary>
    /// A list of components we accept post request on.
    /// </summary>
    public static List<string> PostComponentPages =  new List<string>() { "/counter" };

    // postFormService is injected by the DI
    public HostPageModel(PostFormService postFormService)
    {
        PostFormService = postFormService;
    }

    private PostFormService PostFormService { get; }

    // Hook in to the OnPost.
    public void OnPost()
    {
       if (PostComponentPages.Any(c => Request.Path.ToString().Contains(c)))
           PostFormService.Form = Request.Form; // acceptable component, store the post form in the PostFormService
       else
           throw new Exception("HTTP 401 Error – Unauthorized");
    }

}

PostFormService.cs

Responsible for storing the post data in a service collection.

public class PostFormService
{
    public IFormCollection? Form { get; set; }
}

Program.cs

Register the service.

builder.Services.AddScoped<PostFormService>();

Counter.razor

Inject our service, and read the post data from third party.

@inject PostFormService PostFormService;
protected override void OnInitialized()
{
    base.OnInitialized();
    // These fields are based on the third party.
    var PaymentID = PostFormService.Form?["PaymentID"];
    var ReturnCode = PostFormService.Form?["ReturnCode"];
    var ReturnMessage = PostFormService.Form?["ReturnMessage"];
}

Note: Just noticed this only works in ServerPrerendered. Since the Post request is always treated as a new scope/session, I handle this by making the _Host.cshtml force ServerPrerendered for pages supporting Post requests.

@if (HostPageModel.PostComponentPages.Any(c => Request.Path.ToString().Contains(c)))
{
    <component type="typeof(App)" render-mode="ServerPrerendered" />
}
else
{
    <component type="typeof(App)" render-mode="Server" />
}

Upvotes: 4

Related Questions