Some of my controller methods have the [ValidateAntiForgeryToken] attribute for the usual reasons. It's only app-internal actions, no cross-site API or similar. Now I replaced a request made from JavaScript from jQuery (which seems to send data as form fields) with a real JSON post using fetch() directly. The __RequestVerificationToken field was among the sent data so it must have ended up in a place where ASP.NET Core MVC was looking for it.
Now it's in the JSON body, there are no form fields anymore. And the request fails with code 400, probably due to the missing (not found) token.
I've searched for solutions but this has so far only been covered for the older non-Core ASP.NET from 10 years ago. Is it still possible today with current tools to send the token in the JSON body or as HTTP header (I'm fine with either one) and have it validated without much boilerplate code? I can add a special attribute class for that if needed. I already looked at the framework class but it doesn't do anything, this must be handled elsewhere.
Below is a work demo, you can refer to it. Read this to know more.
1.Customize AntiforgeryOptions in Program.cs:
builder.Services.AddAntiforgery(options =>
{
// Set Cookie properties using CookieBuilder properties†.
options.HeaderName = "X-CSRF-TOKEN-HEADERNAME";
});
2.Require antiforgery validation
public IActionResult Index()
{
// ...
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Privacy()
{
// ...
return View();
}
Index.cshtml:
#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("Privacy")", {
method: "POST",
headers: {
RequestVerificationToken:
document.getElementById("RequestVerificationToken").value
}
});
if (response.ok) {
resultElement.innerText = await response.text();
} else {
resultElement.innerText = `Request Failed: ${response.status}`
}
});
});
</script>
}
result:
Related
I've a onpost method in .net core pagemodel class like below
public void OnPostLoad(string TZ)
{
///..statements..////
}
and i need to call the above method from my razor page - jquery ready function
i've tried like below but it's not worked for me
$(document).ready(function(){
$.post(base_url+"/Transactions/Index?handler=Load="+{TZ:timezone }, response => {
alert("test timezone");
});
});
please let me know if you have any solutions for it. thanks
Request Verification is baked into the Razor Pages framework. If you use ajax to post data, it needs anti-forgery token verification.
1.If your page contains form. The form tag helper injects a hidden form field named __RequestVerificationToken at the end of every form with an encrypted value representing the token. You just need add headers for this token:
<form method="post">
//...
</form>
#section Scripts
{
<script>
$(document).ready(function(){
var timezone = "aaa";
$.ajaxSetup({
headers: {
RequestVerificationToken:$('input:hidden[name="__RequestVerificationToken"]').val()
}
});
$.post(base_url+"/Transactions/Index?handler=Load&TZ=" + timezone ,response => {
alert("test timezone");
});
});
</script>
}
2.If your page does not contain any form, you can turn off anti-forgery token verification and then you no need add RequestVerificationToken header any more:
In ASP.NET 6
builder.Services.AddRazorPages().AddRazorPagesOptions(o =>
{
o.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
});
In ASP.NET Core 3.x or .NET 5
services.AddRazorPages().AddRazorPagesOptions(o =>
{
o.Conventions.ConfigureFilter(new IgnoreAntiforgeryTokenAttribute());
});
3.If your page does not contain form and you also do not want to turn off anti-forgery token verification, you can automatically add #Html.AntiForgeryToken():
#Html.AntiForgeryToken()
#section Scripts
{
<script>
$(document).ready(function(){
var timezone = "aaa";
$.ajaxSetup({
headers: {
RequestVerificationToken:$('input:hidden[name="__RequestVerificationToken"]').val()
}
});
$.post(base_url+"/Transactions/Index?handler=Load&TZ=" + timezone ,response => {
alert("test timezone");
});
});
</script>
}
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:
As soon as I submit this Razor form, the request looks as follows:
with the following payload:
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:
the payload looks good at the first sight:
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:
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 :)
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();
}
Trying over a week so far with no success, to find out why I can't get the response from my controller.
Note I am not fluent in JS, only C#, but I already have a good understanding of what the fetch does.
Now learning ASP.NET Core, but still somewhere under junior level in my overall C# and .Net experience maybe.
It is regarding the process of redirecting to Stripe checkout, but fetch and the method as mentioned, can't agree around 200.
Originally the controller was a normal MVC controller but among many things tried, I made it also an API controller, hoping something which I apparently don't understand, might fix the issue with its method.
Turned over the web upside down searching for similar issues, and I have no other options except reaching to you guys, many experienced devs here.
The method functions 100%, I mean the logic inside. It returns a session object.
The API version returns the session; the MVC version returns this.Json(session);
I know that both versions work because I was debugging them by entering inside after a button click
Its all happening on localhost, therefore I provided credentials: 'include' in the headers... if I am correct.
Trying anything with Postman with authorization of course, but no luck so far.
Firefox Debugging currently throws no errors when going through the fetch.
The Questions:
- Does the fetch function properly according to my code?
- Is the controller prepared to get the payment id from the JSON body of the request?
- When I hardcode the paymentId inside the controller so that I don't expect any parameter, why this doesn't work as well (the fetch has session undefined)?
Here is the fetch in my Pay.cshtml view
<script src="https://js.stripe.com/v3/"></script>
<div id="checkout">
<form id="payment-form">
<div class="row d-flex justify-content-center">
<div class="col-6 py-4 text-center">
<button id="checkout-button"
value="Pay"
class="btn btn-sm btn-labeled btn-primary">
<span class="btn-label">
<ion-icon name="card-outline"></ion-icon>
</span>Pay $ #Model.Amount
</button>
</div>
</div>
</form>
</div>
<script>
var stripe = Stripe('#HomyTestPublishableKey');
var payButton = document.getElementById('checkout-button');
payButton.addEventListener('click', function(event) {
event.preventDefault();
stripe.redirectToCheckout({
sessionId: sessionId
});
});
var sessionId;
var paymentId = '#Model.Id';
fetch("/api/payment/createsession/", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({id: paymentId })
}).then(function(r) {
return r.json();
}).then(function(response) {
sessionId = response.id;
})
.catch(error => console.error('Error:', error));
</script>
The method in the API Controller looks like this:
// Create PaymentSession
[HttpPost]
[Route("createsession")]
public async Task<Session> CreateSession([FromBody]string id)
{
var userId = this.userManager.GetUserId(this.User);
var payment = await this.paymentService.GetPaymentDetailsAsync(id, userId);
var successStringUrl = "https://localhost:44319/Checkout/success?session_id={CHECKOUT_SESSION_ID}";
var cancelStringUrl = "https://localhost:44319/Checkout/cancel";
var options = new SessionCreateOptions
{
PaymentMethodTypes = new List<string>
{
"card",
},
LineItems = new List<SessionLineItemOptions>
{
new SessionLineItemOptions
{
Quantity = 1,
Amount = (long)payment.Amount * 100,
Currency = CurrencyUSD,
Description = $"Payment Id: {payment.Id} for rental at {payment.RentalAddress}",
Name = $"Rent Payment for {DateTime.UtcNow.Month} | {DateTime.UtcNow.Year} for rental at {payment.RentalAddress}",
},
},
PaymentIntentData = new SessionPaymentIntentDataOptions
{
ApplicationFeeAmount = (long)((payment.Amount * 0.01m) * 100),
CaptureMethod = "manual",
TransferData = new SessionPaymentIntentTransferDataOptions
{
Destination = payment.ToStripeAccountId,
},
},
SuccessUrl = successStringUrl,
CancelUrl = cancelStringUrl,
};
var service = new SessionService();
Session session = service.Create(options);
return session; // the MVC version returns Task<IActionResult> of this.Json(session);
}
The options method of Stripe is not important, its only importnat why the fetch can't get anything. and what am I doing fundamentally wrong.
Here is a Stripe engineer CJ Avilla, demonstrating this with PHP and JS but you could get an idea.
After minute mark 20:30 you want to see him creating the fetch and later on, how checkout flow cicks in.
https://www.youtube.com/watch?v=VQ5jccnZ2Ow&t=1160s
Will appreciate any suggestions. Throw them to me.
Anyone with serious experience and a little time to dig in, I can provide access to the private github repo (its my first web project and I have to defend it at the end of the ASP.NET Core course at SoftUni).
Thank you!
P.S.
Edit 1:
Added binding model for the Id param, as I was wrongly expecting string but passing json object. However this still does not prove or allow the method to be accessed by the fetch...
Screen-shot added from my browser debugging.
Edit 2:
This code return Status 400 in console
fetch("https://localhost:44319/api/payment/createsession", {
method: 'POST',
mode: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ id: paymentId })
})
.then(r => r.json().then(data => ({status: r.status, body: data})))
.then(obj => console.log(obj));
why the fetch can't get anything. and what am I doing fundamentally wrong
Your CreateSession action accepts a string-type parameter id, on your JavaScript client side, you can pass data through request body like below.
body: JSON.stringify(paymentId)
Besides, if you'd like to make your action can handle body: JSON.stringify({id: paymentId }) well from client, you can also modify your action to accept a custom model parameter, like below.
[HttpPost]
[Route("createsession")]
public async Task<Session> CreateSession([FromBody]MyTestModel myTestModel)
{
var id = myTestModel.id;
//your code logic here
//...
MyTestModel class
public class MyTestModel
{
public string id { get; set; }
}
let serverResponse = await fetch('https://localhost:{port}/api/payment/createsession/',
{
method: 'POST',
mode: 'cors',
headers: {
'Accept': 'application/json'
},
body: JSON.stringify({ id: paymentId })
}
);
serverResponse.json().then(function (data) {
// server response value come to "data" variable
debugger;
});
So I'm trying to create a comments section for a blog. I'm having trouble identifying the opened blog posts id in my jquery.
I'm getting these errors from the chrome console
GET http://localhost:46223/api/posts//comments
the postid should be inbetween the double slash but its not. When I manually enter the postID inside the ajax call it works perfectly.
An Api Controller is exposing the comments from the database, relevant code below.
[Route("api/posts/{postId:long}/comments")]
public class CommentsController : Controller
{
readonly BlogDataContext _dbContext;
public CommentsController(BlogDataContext db)
{
_dbContext = db;
}
// GET: api/values
[HttpGet]
public IQueryable<Comment> Get(long postId)
{
return _dbContext.Comments.Where(x => x.PostId == postId);
}
When I press the "Show Comments" link it chrome console gives me the error I was talking about earlier. Relevant code from my partial view below. The most important line from below is only the first one.
Show Comments
<div class="comments-container hide">
<h3>Comments</h3>
<div class="comments">
</div>
<hr />
<div>
Add a comment
<div class="new-comment hide">
<form role="form">
<div class="form-group">
<textarea name="Body" class="new-comment form-control" placeholder="Enter comment here..."></textarea>
<button type="submit" class="btn btn-default">Create Comment</button>
</div>
</form>
</div>
</div>
</div>
Relevant code snippets from my .js
$(document).on('click', '.show-comments', function (evt) {
evt.stopPropagation();
new Post(this).showComments();
return false;
});
function Post(el) {
var $el = $(el),
postEl = $el.hasClass('blog-post') ? $el : $el.parents('.blog-post'),
postId = postEl.data('post-id'),
addCommentEl = postEl.find('.add-comment'),
newCommentEl = postEl.find('.new-comment'),
commentEl = newCommentEl.find('[name=Body]'),
commentsContainer = postEl.find('.comments-container'),
commentsEl = postEl.find('.comments'),
showCommentsButton = postEl.find('.show-comments'),
noCommentsEl = postEl.find('.no-comments');
return {
addComment: addComment,
renderComment: renderComments,
showAddComment: showAddComment,
showComments: showComments,
};
function showComments() {
PostCommentService.getComments(postId).then(renderComments);
}
var PostCommentService = (
function PostCommentService() {
function call(postId, method, data) {
return $.ajax({
// RESTful Web API URL: /api/posts/{postId}/comments
url: ['/api/posts', postId, 'comments'].join('/'), // If I Change the 'postId' here to an integer of an existing postId, it works perfectly.
type: method,
data: JSON.stringify(data),
contentType: 'application/json'
});
}
return {
// Add comment by calling URL with POST method and passing data
addComment: function (comment) {
return call(comment.PostId, 'POST', comment);
},
// Get comments by calling URL with GET method
getComments: function (postId) {
return call(postId, 'GET');
}
};
})();
Full .js file
I'm sorry if I missed to include something, but I have a lot of code. If you need to know anything else let me know.
I'd also be grateful just for some suggestions where my error might be.
Your code is getting the post id from the data attribute post-id of the postEl. postEl could be the same anchor tag which was clicked or it's parent with blog-post css class.
var $el = $(el),
postEl = $el.hasClass('blog-post') ? $el : $el.parents('.blog-post'),
postId = postEl.data('post-id'),
But in your HTML markup, there is no such data attribute for the anchor tag. So if you add that, your code will be able to get the post id and use that to build the url
Show Comments
I hard coded 250 as the value for the data-post-id attribute. You may replace it with a value coming from your model.
Show Comments
Is there any way to get the name of View that called method in controller and save it for example in some custom variable inside that controller's method?
For example:
I have one View that uses Ajax to get to InfinateScroll method in controller:
<div class="container-post">
<div id="postListDiv">
#{Html.RenderAction("PostList", "Posts", new { Model = Model });}
</div>
<div id="loadingDiv" style="text-align: center; display: none; margin-bottom: 20px;">
<img alt="Loading" src="#Url.Content("~/images/ajax-loader.gif")" />
</div>
</div>
<script src="#Url.Content("~/Scripts/jquery-1.10.2.min.js")"></script>
<script type="text/javascript">
var BlockNumber = 2;
var NoMoreData = false;
var inProgress = false;
$(window).scroll(function () {
if ($(window).scrollTop() == $(document).height() - $(window).height() && !NoMoreData && !inProgress) {
inProgress = true;
$("#loadingDiv").show();
$.post("#Url.Action("InfinateScroll", "Posts")", { "BlockNumber": BlockNumber },
function (data) {
BlockNumber = BlockNumber + 1;
NoMoreData = data.NoMoreData;
$("#postListDiv").append(data.HTMLString);
$("#loadingDiv").hide();
inProgress = false;
});
}
});
</script>
I use this View on two pages. In one case I'm using it to show only posts from specific user (user who is logged in), and on the other view I'm showing posts from all users in database(similar to Facebook wall where you can see only your post, and NewsFeed where you can not only your's but also posts from your frineds).
For some reason I would like to know which page was active when call for InfinateScroll method was made.
This is the method where I would like to make some differences between those two pages so I can do some check out's later.
[HttpPost]
public ActionResult InfinateScroll(int BlockNumber)
{
int BlockSize = 5;
var posts = PostManager.GetPosts(BlockNumber, BlockSize);
JsonModel jsonModel = new JsonModel();
jsonModel.NoMoreData = posts.Count < BlockSize;
jsonModel.HTMLString = RenderPartialViewToString("PostList", posts);
return Json(jsonModel);
}
This method gets posts using helper method GetPosts and it's used for showing more posts on scroll.
You can get the name of the current View from inside the view using the following:
#Path.GetFileNameWithoutExtension(Server.MapPath(VirtualPath))
Source: How to get the current view name in asp.net MVC 3?
so you could add this as a routevalue into your #Url.Action like so:
#Url.Action(
"InfinateScroll",
"Posts",
new{callingView=Path.GetFileNameWithoutExtension(Server.MapPath(VirtualPath))})
Then you could add a parameter to your controller method
public ActionResult InfinateScroll(int BlockNumber, string callingView)
You can create a hidden variable in the html like this -
<input type="hidden" id="pageName" value="myPage1" />
Add an extra parameter to your Action -
public ActionResult InfiniteScroll(int BlockNumber, int pageName)
And then, in your jquery code, when you post, send in pageName as well.
$.post("#Url.Action("InfinateScroll", "Posts")", { "BlockNumber": BlockNumber, "pageName": $('#pageName').val() },
Hope this helps.
In one case I'm using it to show only posts from specific user... and
on the other view I'm showing posts from all users in database...
Putting your desired logic on the view is unsafe, especially if showing data is user-based or user-specific. However, if you insists on having the logic on the view then you should pass along another variable to the controller like so:
$.post("#Url.Action("InfinateScroll", "Posts")",
{ "BlockNumber": BlockNumber, "UserId": userId },
// rest of your code goes here...
});
You then should have another parameter in your controller:
[HttpPost]
public ActionResult InfinateScroll(int BlockNumber, int userId)
{
//filter your data based on the "userId" parameter
}
But like I mentioned this is unsafe because someone can easily pass in a valid "userId" and get to the data when you don't want them to. So the safest (or safer) way is to have the "filtering logic" in your controller like so:
[HttpPost]
public ActionResult InfinateScroll(int BlockNumber)
{
// a context based logic
var userId = GetLoggedInUserId();
// that method could return null or zero
// and depending on how you approach it
//filter your data based on the "userId"
}