Submitting React form to ASP.NET 6 Identity controller - where to get RequestVerificationToken from?

Viewed 444

I'm working with an ASP.NET 6 app, generated with ASP.NET Core with React.js Visual Studio 2022 template. I've used Individual Accounts as Authentication Type when creating the project, so all Identity stuff has been nicely generated.

Now I have nice Razor views scaffolded by ASP.NET's Identity. However, I'd like to build my whole UI as React SPA application, using react-router. It means that I don't want to use Razor views, but still use ASP.NET's Identity backend.

Firstly, I wanted to implement a React form to submit changing the user password. Razor view generated for that is Identity/Pages/Account/ManageChangePassword.cshtml. It looks like that:

change password Razor form

As soon as I submit this Razor form, the request looks as follows:

razor change pwd form request

with the following payload:

enter image description here

So now, I basically rebuilt this form in React:

import React, { useState } from "react";
import Button from "react-bootstrap/Button";
import Form from "react-bootstrap/Form";

export const ChangePassword = () => {
  const [currentPassword, setCurrentPassword] = useState<string>("");
  const [newPassword, setNewPassword] = useState<string>("");
  const [newPasswordConfirm, setNewPasswordConfirm] = useState<string>("");

  const onChangePasswordFormSubmit = (e: React.FormEvent) => {
    e.preventDefault();

    const formData = new FormData();
    formData.append("Input.OldPassword", currentPassword);
    formData.append("Input.NewPassword", newPassword);
    formData.append("Input.ConfirmPassword", newPasswordConfirm);

    fetch("Identity/Account/Manage/ChangePassword", {
      method: "POST",
      body: formData,
    });
  };

  return (
    <Form onSubmit={onChangePasswordFormSubmit}>
      <Form.Group className="mb-3" controlId="currentPassword">
        <Form.Label>Current password</Form.Label>
        <Form.Control
          type="password"
          placeholder="Current password"
          value={currentPassword}
          onChange={(e) => setCurrentPassword(e.target.value)}
        />
      </Form.Group>
      <Form.Group className="mb-3" controlId="newPassword">
        <Form.Label>New password</Form.Label>
        <Form.Control
          type="password"
          placeholder="New password"
          value={newPassword}
          onChange={(e) => setNewPassword(e.target.value)}
        />
      </Form.Group>
      <Form.Group className="mb-3" controlId="newPasswordConfirm">
        <Form.Label>Confirm new password</Form.Label>
        <Form.Control
          type="password"
          placeholder="Confirm new password"
          value={newPasswordConfirm}
          onChange={(e) => setNewPasswordConfirm(e.target.value)}
        />
      </Form.Group>
      <Button variant="primary" type="submit">
        Change password
      </Button>
    </Form>
  );
};

However, when submitting this form, I'm getting a HTTP 400 error:

enter image description here

the payload looks good at the first sight:

enter image description here

but I noticed that I'm missing the __RequestVerificationToken in this payload. I guess it's coming from the fact that Identity controllers (to which I have no access) must be using [ValidateAntiForgeryToken] attribute.

If I change my form's submit code to add this payload parameter manually:

const formData = new FormData();
    formData.append("Input.OldPassword", currentPassword);
    formData.append("Input.NewPassword", newPassword);
    formData.append("Input.ConfirmPassword", newPasswordConfirm);
    formData.append(
      "__RequestVerificationToken",
      "CfDJ8KEnNhgi1apJuVaPQ0BdQGnccmtpiQ91u-6lFRvjaSQxZhM6tj8LATJqWAeKFIW5ctwRTdtQruvxLbhq2EVR3P1pATIyeu3FWSPc-ZJcpR_sKHH9eLODiqFPXYtdgktScsOFkbnnn5hixMvMDADizSGUBRlSogENWDucpMgVUr3nVMlGwnKAQDH7Ck4cZjGQiQ"
    );

    fetch("Identity/Account/Manage/ChangePassword", {
      method: "POST",
      body: formData,
    });
  };

It works fine and the request arrives correctly.

My question is: where to get __RequestVerificationToken from? How can I send it to the ASP.NET's Identity controller from a purely React form?

I noticed that when submitting my React form, this value is visible in cookies: cookies for react form submit

so the React form/browser must somehow know this value? Where does it come from? Maybe my approach is somehow wrong here? Thanks for advising :)

2 Answers

The AntiForgeryToken is generated by

HtmlHelper.AntiForgeryToken();
AntiForgery.GetHtml();
AntiForgeryWorker.GetFormInputElement();

And is validated by

AntiForgery.Validate();
AntiForgeryWorker.Validate();

I'd send AntiForgery.GetHtml() to the client, and then validate it on the server.

Maybe you can even create an ajax endpoint that returns new tokens to the Client.

I suspect your are missing the cookie, can you please check if you configured your cookies for your anti forgery in your

  public void ConfigureServices(IServiceCollection services)
    {
        // make sure you add the cookie name
        services.AddAntiforgery(o => {
            o.Cookie.Name = "X-CSRF-TOKEN";
        });
        
    }

or more from here on Microsoft Ref, theres stuff on doing it for SPA apps all well further below

builder.Services.AddAntiforgery(options =>
{
    // Set Cookie properties using CookieBuilder properties†.
    options.FormFieldName = "AntiforgeryFieldname";
    options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
    options.SuppressXFrameOptionsHeader = false;
});

SPA App

@inject Microsoft.AspNetCore.Antiforgery.IAntiforgery Antiforgery

@{
    ViewData["Title"] = "JavaScript";

    var requestToken = Antiforgery.GetAndStoreTokens(Context).RequestToken;
}

<input id="RequestVerificationToken" type="hidden" value="@requestToken" />

<button id="button" class="btn btn-primary">Submit with Token</button>
<div id="result" class="mt-2"></div>

@section Scripts {
<script>
    document.addEventListener("DOMContentLoaded", () => {
        const resultElement = document.getElementById("result");

        document.getElementById("button").addEventListener("click", async () => {

            const response = await fetch("@Url.Action("FetchEndpoint")", {
                method: "POST",
                headers: {
                    RequestVerificationToken:
                        document.getElementById("RequestVerificationToken").value
                }
            });

            if (response.ok) {
                resultElement.innerText = await response.text();
            } else {
                resultElement.innerText = `Request Failed: ${response.status}`
            }
        });
    });
</script>
}

To Debug it, and see if it works

[IgnoreAntiforgeryToken]
public IActionResult IndexOverride()
{
    // ...

    return RedirectToAction();
}
Related