skip to Main Content

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


  1. 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.

    Login or Signup to reply.
  2. 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();
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search