In this hypothetical scenario, I am displaying a list of Messages on a page.
A snip of javascript - Edit(id)
- retrieves from the server the correct HTML for editing a message, and injects it on the page.
Another snip of javascript - Save(id)
- does an AJAX POST back to the server and injects the result back on the page.
According to the AntiForgery rules of Razor Pages, the POST triggers a 404 because it's missing the correct token. Since I'm not using a <form>
, the built-in antiforgery framework doesn't kick in automatically and provide the token.
And, since I have the option of, in theory, editing multiple message in parallel, I'm not sure how to correctly implement a @Html.AntiForgeryToken()
field. Do I generate a token per message, and then somehow scoop it up and send it along with my POST, or...?
I'm wondering if there is a correct way of handling this scenario that takes AntiForgery into account, or if I just have to suck it up and build around it? I can work around the problem easily enough with [IgnoreAntiforgeryToken]
but this obviously has its own consequences.
For the time being, MessageManager.Messages
is just a static List<TextMessage>
@foreach (var item in Model.Messages) {
<div id="message_@item.Id">
<label class="form-label">@item.Type:</label>
@{
var result = Markdown.ToHtml(item.Message, pipeline);
@Html.Raw(result)
}
</div>
<div class="mb-3 text-end">
<a href="#" onclick="Edit(@item.Id)">✏</a>
<a href="#" onclick="Delete(@item.Id)">❌</a>
</div>
}
@section Scripts {
<script>
function Edit(messageId) {
$.get(
"/Messages/_EditMessage?messageId=" + messageId,
function (str) {
$('#message_' + messageId).html(str);
}
);
}
function Save(messageId) {
$.post(
"/Messages/_EditMessage?messageId=" + messageId,
{ NewMessage: $('#NewMessage_' + messageId).val() },
function (str) {
$('#message_' + messageId).html(str);
}
);
}
</script>
}
@if (Model.Edit) {
<div class="mb-3">
<label class="form-label">@Model.Message.Type</label>
<textarea class="form-control" id="NewMessage_@Model.Message.Id" rows="@(Model.Message.Message.Count(c => c == '\n') + 1)">@Model.Message.Message</textarea>
</div>
<div>
<button class="btn btn-outline-success" onclick="Save(@Model.Message.Id)">Save</button>
</div>
}
else {
var result = Markdown.ToHtml(Model.Message.Message, pipeline);
<label class="form-label">@Model.Message.Type:</label>
@Html.Raw(result)
}
[IgnoreAntiforgeryToken]
public class _EditMessageModel : PageModel
{
public TextMessage Message { get; set; } = null!;
public bool Edit { get; set; }
[BindProperty] public string NewMessage { get; set; } = string.Empty;
public IActionResult OnGet(int messageId) {
var message = MessageManager.Messages.FirstOrDefault(m => m.Id == messageId);
if (message is null)
return NotFound(messageId);
Message = message;
Edit = true;
return Page();
}
public IActionResult OnPost(int messageId) {
var message = MessageManager.Messages.FirstOrDefault(m => m.Id == messageId);
if (message is null)
return NotFound(messageId);
Message = message;
Message.Message = NewMessage;
Edit = false;
return Page();
}
}
It really was that simple.
Add @Html.AntiForgeryToken()
to the bottom of the view page, and replace the Save()
function with
function Save(messageId) {
$.ajax({
type: "POST",
headers: { "RequestVerificationToken": $('input[name="__RequestVerificationToken"]').val() },
url: "/Messages/_EditMessage?messageId=" + messageId,
data: { NewMessage: $('#NewMessage_' + messageId).val() }
}).done(function(response){
$('#message_' + messageId).html(response);
});
}
It doesn't seem to complain about re-using the same token to submit multiple POSTs.
Do I generate a token per message, and then somehow scoop it up and send it along with my POST, or...?
This question is split into two parts to avoid confusion as there's a 'yes' part and a 'no' part in the answer.
Do I generate a token per message...
No, you do not need to generate a token per message. The HTML Helper @Html.AntiForgeryToken()
needs to be added once in the body of a Razor view and the token generated by the AntiForgeryToken
Helper can be reused on the same page.
If the table in your View.cshtml
sample was contained within a <form method="post"></form>
element, the FormTagHelper
adds the hidden anti-forgery field. Your View.cshtml
sample does not have a form
element.
...and then somehow scoop it up and send it along with my POST?
Yes, you need to retrieve the token generated by @Html.AntiForgeryToken()
and add it to each POST
request made from the client-side page via JavaScript/AJAX.
Per this page on learnrazorpages.com:
If you omit the value from the request, the server will return a 400 Bad Request result.
This SO answer provides a function to call in the save()
function from your question. On post
, the function retrieves the anti-forgery token from the hidden input
field created by @Html.AntiForgeryToken()
. Add @Html.AntiForgeryToken()
in the body of your View.cshtml
page.
For simplicity, the token is retrieved on every call to the save()
function. You can, however, call the addAntiForgeryToken()
function once in JavaScript, store its value, and then append the token value on each subsequent post
request.
@section Scripts {
<script>
function Edit(messageId) {
$.get(
"/Messages/_EditMessage?messageId=" + messageId,
function (str) {
$('#message_' + messageId).html(str);
}
);
}
function Save(messageId, content) {
let data = { NewMessage: messageId };
data = addAntiForgeryToken(data);
console.log(data);
$.post(
"/Messages/_EditMessage?messageId=" + messageId,
data,
function (str) {
$('#message-response').html(str);
}
);
}
// CSRF (XSRF) security
// https://stackoverflow.com/questions/73037094/how-to-do-an-ajax-post-with-mvc-antiforgerytoken
function addAntiForgeryToken(data) {
//if the object is undefined, create a new one.
if (!data) {
data = {};
}
//add token
const tokenInput = $('input[name=__RequestVerificationToken]');
if (tokenInput.length) {
data.__RequestVerificationToken = tokenInput.val();
}
return data;
}
</script>
}