MVC download of clickonce Setup.exe and display confirmation view - asp.net-mvc

I'll start by saying i'm a C# MVC newbie, but I've set up a site with Identity Management and extended the database with some custom tables to store additional info about my users, so I'm not a total neophyte. I've been working on a VB WPF application that I want to deploy from my new website and that is where I'm running into an issue.
I've created a new controller (User) and a couple of views (Download) & (Setup). I created a downloadmodel used by the download view.
In abstract what I am doing is displaying the download view (get) which has three checkboxes to confirm the user has read the Overview, Installation, and Terms of Service. These are boolean values in the model. I also have a string response message in the model, that displays just above the submit button. Here is the model:
public class DownloadModel
{
public bool Overview { get; set; }
public bool Installation { get; set; }
public bool TermsOfService { get; set; }
public string Response { get; set; }
public DownloadModel()
{
Overview = false;
Installation = false;
TermsOfService = false;
Response = "After checking boxes click the button to begin installation";
}
}
My User Controller handles the Get to initially display the download view, and then in the Post it checks to see if all the checkboxes were ticked, if not it updates the response message and returns the view.
If the checkboxes are all checked then it pulls the subscriber (which must exist because it was created when the user verified their e-mail via the account controller - identity management), then proceeds to update the subscriber with the original (if new) or last download date(s). At this point I want to begin downloading the clickonce setup.exe file, before returning the setup view.
[Authorize]
public class UserController : Controller
{
// GET: User/Download
public ActionResult Download()
{
return View(new DownloadModel { });
}
// Post: User/Download
[HttpPost]
public ActionResult Download(DownloadModel downloadcheck)
{
if (!ModelState.IsValid)
{
return View(downloadcheck);
}
//check to see if all the boxes were checked
if (downloadcheck.Overview == true &
downloadcheck.Installation == true &
downloadcheck.TermsOfService == true)
{
//yes - so let's proceed
//first step is to get the subscriber
Subscriber tSubscriber = new Subscriber();
tSubscriber.Email = User.Identity.Name;
bool okLoad = tSubscriber.LoadByEmail();
if (okLoad == false)
{
//we have a real problem. a user has logged in but they are not yet
//a valid subscriber?
throw new Exception("Subscriber not found");
}
// update subscriber with download in process...
if (tSubscriber.OriginalDownload == DateTime.MinValue)
{
tSubscriber.OriginalDownload = DateTime.Now;
tSubscriber.LastDownload = tSubscriber.OriginalDownload;
}
else
{
tSubscriber.LastDownload = DateTime.Now;
}
if (tSubscriber.UpdateDownloaded() == false)
{
//update of download dates failed
//another problem that shouldnt occur.
downloadcheck.Response = "A problem occured downloading your setup."
+ "Try again. If this error continues please contact support.";
return View(downloadcheck);
}
//download dates have been updated for the subscriber so let's start the download!
//THIS IS WHERE I NEED TO BEGIN THE DOWNLOAD
return View("Setup");
}
else
{
// all boxes were not checked - update message
downloadcheck.Response = "Please confirm you have reviewed the above information "
+ "by checking all of the boxes before clicking on the button.";
return View(downloadcheck);
}
}
}
The download view is pretty straight forward, and the setup view simply confirms the download was started and provides a link to the help-setup page.
I'm really a bit lost here. I thought I'd plug in a return new filepathresponse, but I can't do that and return the setup view.
My other thought was to somehow trigger the download of my /xxx/setup.exe from within the setup view as it is returned - but I'm at a loss as to how to accomplish this.
I'll be the first to admit that my mvc c# code is probably overly verbose and my approach to how I've done this may be totally wrong, but I'm just scrambling to get this done so I can deploy my WPF app to select Beta users for testing. It's been a long time living off savings and I can smell go-live from here.
One final note, I'm using setup.exe clickonce deployment of my wpf app for simplicity, as there are .net and localsqldb prerequisites, but I will not be using automated updates - not that this is really relevant.
Appreciate all input and advice.

After more digging and hacking I've found a solution that works. Firstly in my setup view (confirmation page) I added a simple script to initiate a new function in my user controller:
<script type="text/javascript">
window.location.href = "/user/sendfile/"
</script>
The controller change was simple too. For testing I just used a txt file.
// User/SendFile
public ActionResult SendFile()
{
string path = #"~/xxxx/anyfile.txt";
string content = "application/txt";
//string content = "application/x-ms-application";
return new FilePathResult(path, content)
{
FileDownloadName = "mynewtext.txt"
};
}
What is really interesting about this solution is that the FileDownloadName is what the file content is downloaded as. So in this way I can refer to the actual setup.exe file in the path, but then name the downloaded exe anything I want. Bonus :)

Related

Image rendering via controller API sometimes breaks when being access by multiple users

I have here an image rendering method on my api that is being access via url by users in order to display images as source. I don't have a problem when testing locally since I'm only making single user requests. But by the time I deployed it, staging/production testers comes in. The image being rendered is broken because of an internal server error, I'm guessing it is because of the multiple request.
Here's how I do it:
[Route("userprofileimage")]
public class UserProfileImageController : Controller
{
[Route("render")]
public FileResult Render(string di, string tk, string fi)
{
if (!string.IsNullOrEmpty(di) && !string.IsNullOrEmpty(tk) && !string.IsNullOrEmpty(fi))
{
var filePath = Path.Combine(Path.Combine(Server.MapPath("~/App_Data/UserProfileImages"), fi));
var isValid = new UserTokenService().ValidateUserToken(Guid.Parse(di), tk);
if(isValid)
{
if (ImageFileNotAvailable(filePath))
return new FileStreamResult(new FileStream(Path.Combine(Server.MapPath("~/App_Data/UserProfileImages"), "default.png"), FileMode.Open), "image/png");
else
return new FileStreamResult(new FileStream(filePath, FileMode.Open), "image/png");
}
}
return new FileStreamResult(new FileStream(Path.Combine(Server.MapPath("~/App_Data/UserProfileImages"), "default.png"), FileMode.Open), "image/png");
}
private bool ImageFileNotAvailable(string fullFilePath)
{
return !System.IO.File.Exists(fullFilePath);
}
}
As you can see there are token and id checks before the actual rendering of the image.
I have no specific reason on my approach on this one. I just find it convenient to have links as image sources.
I was expecting that the requests from multiple users will wait until a previous execution is done before going in but I guess they go in simultaneously. So at some point the file is currently open or being manipulated and another request comes in. Hence the error.
Any ideas on how can I improve this?

Multiple views modifying one object MVC

I am building a service which requires a somewhat lengthy setup process. I have it broken into 4 models and 4 corresponding views. They are Setup, Setup2, Setup3, and Setup4. Each of these views gathers information from the user which is stored in a User object. I have been passing the user along like this:
[HttpPost]
public ActionResult Setup(FormCollection values)
{
User registeringUser = new User();
registeringUser.email = User.Identity.Name;
registeringUser.fName = values["fName"];
registeringUser.lName = values["lName"];
registeringUser.phone = values["phone"];
return RedirectToAction("/Setup2", registeringUser);
}
For some reason, this seems to work just fine for the first jump (from Setup to Setup2) but after that I'm getting weird behavior, such as User. getting set to null when the User is passed to another View.
In a related, but slightly different issue, I need the last screen (Setup4) to be recursive. This screen adds a course in which the user is enrolled, and if they don't check the "This was my last class" button, it needs to basically clear the form so they can enter another course.
The entire Controller looks like this:
[HttpPost]
public ActionResult Setup4(FormCollection values, User registeringUser)
{
// values["allClassesAdded"] returns "false" as a string if box is unchecked, returns "true,false" if checked.
// Solution: parse string for "true"
if (utils.parseForTrue(values["allClassesAdded"]))
{
// TODO Redirect to "congratulations you're done" page.
database.CreateUserInDB(registeringUser);
return Redirect("/Home");
}
else
{
// Build course and add it to the list in the User
course c = new course(values);
if (Request.IsAuthenticated)
{
//registeringUser.currentCourses.Add(c);
registeringUser.AddCourse(c);
return RedirectToAction("/Setup4", registeringUser); // <---- This doesn't really work right
//return View();
}
else
{
return Redirect("/Account/Login");
}
}
}
This is my first project with MVC, so if you find that I'm doing the entire thing completely incorrectly, feel free to not answer the question I asked and offer the proper solution to this need. I'm moving an existing (pure) C# project to MVC and I'm mainly just stuck on how to work within MVC's interesting structure. I'm very grateful for any help you can give!
Thanks!
You can store user related data in session without passing it between requests
Smth like this
[HttpPost]
public ActionResult Step1(Step1Model model)
{
Session["UserRegistration"] = new UserRegistration
{
FirstName = model.fName,
....
}
....
}
[HttpPost]
public ActionResult Step2(Step2Model model)
{
var userRegistration = Session["UserRegistration"] as UserRegistration;
if (userRegistration == null) { return Redirrect("Step1"); }
userRegistration.SomeField = model.someField;
...
Session["UserRegistration"] = userRegistration;
....
}

ASP.NET MVC: Keeping last page state

Here's the situation: i have a SearchPage where an user can make a complex search. Nothing really unusual. After the results are displayed, the user can select one of them and move to another Page (Like a Master/Detail).
I have a breacrumb which holds the places where the user has been and it can have more than 4 levels (Like Main -> 2Page -> 3Page -> 4Page -> NPage). What i want is to maintain the state of each control on my complex search page, if the user uses the breacrumb to navigate backwards, since i don't want them to manually set all those search filters again.
So far, i've been using javascript:history.back(), but since i can have multiple levels on my breadcrumb, this hasn't been very useful. I was thinking about using OutputCache to do it, but i don't know how i would proceed.
UPDATE
I've just talked to a co-worker and he told me that some of our combobox (dropdownlist) are dynamically generated. So if the user select one item on the first combobox, the second will be filled with data related to the first selection.
OutputCache would cache the results for every user. Why don't you try to store the information in a cookie with page url and filter information. Each time an action is executed, read the cookie and populate the model (custom model for search) with those values found (if they match the page url, action in this situation). Pass the model to the view and let it repopulate the search criteria text boxes and check boxes.
UPDATE:
When a user fills in the search filter text boxes, you are passing that information back to a controller somehow. Probably as some kind of a strongly typed object.
Let's say your users get to enter the following information:
- Criteria
- StartDate
- EndDate
There is a model called SearchCriteria defined as:
public class SearchCriteria
{
public string Criteria { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
}
Your action could look something like this:
[HttpGet]
public ViewResult Search()
{
SearchCriteria criteria = new SearchCriteria();
if (Request.Cookies["SearchCriteria"] != null)
{
HttpCookie cookie = Request.Cookies["SearchCriteria"];
criteria.Criteria = cookie.Values["Criteria"];
criteria.StartDate = cookie.Values["StartDate"] ?? null;
criteria.EndDate = cookie.Values["EndDate"] ?? null;
}
return View(criteria);
}
[HttpPost]
public ActionResult Search(SearchCriteria criteria)
{
// At this point save the data into cookie
HttpCookie cookie;
if (Request.Cookies["SearchCriteria"] != null)
{
cookie = Request.Cookies["SearchCriteria"];
cookie.Values.Clear();
}
else
{
cookie = new HttpCookie("SearchCriteria");
}
cookie.Values.Add("Criteria", criteria.Criteria);
if (criteria.StartDate.HasValue)
{
cookie.Values.Add("StartDate", criteria.StartDate.Value.ToString("yyyy-mm-dd"));
}
if (criteria.EndDate.HasValue)
{
cookie.Values.Add("EndDate", criteria.EndDate.Value.ToString("yyyy-mm-dd"));
}
// Do something with the criteria that user posted
return View();
}
This is some kind of a solution. Please understand that I did not test this and I wrote it from top of my head. It is meant to give you an idea just how you might solve this problem. You should probably also add Action to SearchCriteria so that you can check whether this is an appropriate action where you would read the cookie. Also, reading and writing a cookie should be moved into a separate method so that you can read it from other actions.
Hope this helps,
Huske

2-Step Authentication in ASP.Net MVC

I am working on an ASP.Net Mvc 3 application using FormsAuthentication with a custom MembershipProvider (so I do have some control over what the provider returns).
The requirements mandate a 2-step authentication process (username and password followed by secret question). A user should not be able to access any of the "secure" sections of the site without passing both steps. Please don't mention whether this is multi-factor security or not, I already know.
Please provide a recommendation on how to best accomplish this task.
Here are some considerations:
I am allowed (architecturally) to use session - would prefer not to.
I would prefer to use the out-of the box [Authorize] ActionFilter for Controllers providing secure content.
The people in charge would like for the url for the 2 steps to be the same: i.e. www.contoso.com/login/. In my attempts at least, this has caused some minor-but-not-insignificant issues when users enter an incorrect answer in the second step (they are not officially logged in, but I need to ensure that I am still working against the half-authenticated user's secret question/answer).
Thanks.
Use a custom View Model in conjunction with hidden form fields. Just make sure it's all done over https.
ViewModel
public LoginForm
{
public string UserName { get; set; }
public string Password { get; set; }
public int SecretQuestionId { get; set; }
public string SecretQuestion { get; set; }
public string SecretQuestionAnswer { get; set; }
}
Action Methods
public ActionResult Login()
{
var form = new LoginForm();
return View(form);
}
[HttpPost]
public ActionResult Login(LoginForm form)
{
if (form.SecretQuestionId == 0)
{
//This means that they've posted the first half - Username and Password
var user = AccountRepository.GetUser(form.UserName, form.Password);
if (user != null)
{
//Get a new secret question
var secretQuestion = AccountRepository.GetRandomSecretQuestion(user.Id);
form.SecretQuestionId = secretQuestion.Id;
form.SecretQuestion = secretQuestion.QuestionText;
}
}
else
{
//This means that they've posted from the second half - Secret Question
//Re-authenticate with the hidden field values
var user = AccountRepository.GetUser(form.UserName, form.Password);
if (user != null)
{
if (AccountService.CheckSecretQuestion(form.SecretQuestionId, form.SecretQuestionAnswer))
{
//This means they should be authenticated and logged in
//Do a redirect here (after logging them in)
}
}
}
return View(form);
}
View
<form>
#if (Model.SecretQuestionId == 0) {
//Display input for #Model.UserName
//Display input for #Model.Password
}
else {
//Display hidden input for #Model.UserName
//Display hidden input for #Model.Password
//Display hidden input for #Model.SecretQuestionId
//Display #Model.SecretQuestion as text
//Display input for #Model.SecretQuestionAnswer
}
</form>
If you're not happy with sending the username and password back to the view in hidden fields to re-authenticate and make sure they're not cheating... you could create a HMAC or something like that to test.
Btw, this question seems like a few questions rolled into one... so just answered how to do 2-step authentication with one view / action method.
I would probably do something where the first step makes them enter a username and password. Check it, if its good, move them along to an authorize flagged view that asks them to put in the answer to the question. If they fail that, sign them out, boot them out, whatever. I don't think this is possible in one view, unless you render a partial view and if they leave without finishing the authentication process, you sign them out and clear their cookie.
----EDIT-----
On second thought, you could do a partial view, just dont formsauth sign them in until they complete the second part of the partial view. Some psuedo code:
public ActionResult Login(){
get username and password off the view
if its valid
render a partial view that asks for the secret answer
if thats valid
forms auth login
else
try again, get booted, or whatever
else
get booted, try again, whatever
}

How do I prevent multiple form submission in .NET MVC without using Javascript?

I want to prevent users submitting forms multiple times in .NET MVC. I've tried several methods using Javascript but have had difficulties getting it to work in all browsers. So, how can I prevent this in my controller? It there some way that multiple submissions can be detected?
Updated answer for ASP.NET Core MVC (.NET Core & .NET 5.0)
Update note: Remember ASP.NET Core is still called "Core" in .NET 5.0.
I'm going to stick to the least-impact use case like before, where you're only adorning those controller actions that you specifically want to prevent duplicate requests on. If you want to have this filter run on every request, or want to use async, there are other options. See this article for more details.
The new form tag helper now automatically includes the AntiForgeryToken so you no longer need to manually add that to your view.
Create a new ActionFilterAttribute like this example. You can do many additional things with this, for example including a time delay check to make sure that even if the user presents two different tokens, they aren't submitting multiple times per minute.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext context) {
if (context.HttpContext.Request.HasFormContentType && context.HttpContext.Request.Form.ContainsKey("__RequestVerificationToken")) {
var currentToken = context.HttpContext.Request.Form["__RequestVerificationToken"].ToString();
var lastToken = context.HttpContext.Session.GetString("LastProcessedToken");
if (lastToken == currentToken) {
context.ModelState.AddModelError(string.Empty, "Looks like you accidentally submitted the same form twice.");
}
else {
context.HttpContext.Session.SetString("LastProcessedToken", currentToken);
}
}
}
}
By request, I also wrote an asynchronous version which can be found here.
Here's a contrived usage example of the custom PreventDuplicateRequest attribute.
[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public IActionResult Create(InputModel input) {
if (ModelState.IsValid) {
// ... do something with input
return RedirectToAction(nameof(SomeAction));
}
// ... repopulate bad input model data into a fresh viewmodel
return View(viewModel);
}
A note on testing: simply hitting back in a browser does not use the same AntiForgeryToken. On faster computers where you can't physically double click the button twice, you'll need to use a tool like Fiddler to replay your request with the same token multiple times.
A note on setup: Core MVC does not have sessions enabled by default. You'll need to add the Microsoft.AspNet.Session package to your project, and configure your Startup.cs properly. Please read this article for more details.
Short version of Session setup is:
In Startup.ConfigureServices() you need to add:
services.AddDistributedMemoryCache();
services.AddSession();
In Startup.Configure() you need to add (before app.UseMvc() !!):
app.UseSession();
Original answer for ASP.NET MVC (.NET Framework 4.x)
First, make sure you're using the AntiForgeryToken on your form.
Then you can make a custom ActionFilter:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
if (HttpContext.Current.Request["__RequestVerificationToken"] == null)
return;
var currentToken = HttpContext.Current.Request["__RequestVerificationToken"].ToString();
if (HttpContext.Current.Session["LastProcessedToken"] == null) {
HttpContext.Current.Session["LastProcessedToken"] = currentToken;
return;
}
lock (HttpContext.Current.Session["LastProcessedToken"]) {
var lastToken = HttpContext.Current.Session["LastProcessedToken"].ToString();
if (lastToken == currentToken) {
filterContext.Controller.ViewData.ModelState.AddModelError("", "Looks like you accidentally tried to double post.");
return;
}
HttpContext.Current.Session["LastProcessedToken"] = currentToken;
}
}
}
And on your controller action you just...
[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public ActionResult CreatePost(InputModel input) {
...
}
You'll notice this doesn't prevent the request altogether. Instead it returns an error in the modelstate, so when your action checks if ModelState.IsValid then it will see that it is not, and will return with your normal error handling.
I've tried several methods using Javascript but have had difficulties getting it to work in all browsers
Have you tried using jquery?
$('#myform').submit(function() {
$(this).find(':submit').attr('disabled', 'disabled');
});
This should take care of the browser differences.
Just to complete the answer of #Darin, if you want to handle the client validation (if the form has required fields), you can check if there's input validation error before disabling the submit button :
$('#myform').submit(function () {
if ($(this).find('.input-validation-error').length == 0) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
What if we use $(this).valid()?
$('form').submit(function () {
if ($(this).valid()) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
Strategy
The truth is that you need several lines of attack for this problem:
The Post/Redirect/Get (PRG) pattern is not enough by itself. Still, it should always be used to provide the user with good experiences when using back, refresh, etc.
Using JavaScript to prevent the user from clicking the submit button multiple times is a must because it provides a much less jarring user experience compared to server-side solutions.
Blocking duplicate posts solely on the client side doesn't protect against bad actors and does not help with transient connection problems. (What if your first request made it to the server but the response did not make it back to the client, causing your browser to automatically resend the request?)
I'm not going to cover PRG, but here are my answers for the other two topics. They build upon the other answers here. FYI I'm using .NET Core 3.1.
Client-Side
Assuming you are using jQuery validation, I believe this is the cleanest/most efficient way to prevent your form submit button from being double-clicked. Note that submitHandler is only called after validation has passed, so there is no need to re-validate.
$submitButton = $('#submitButton');
$('#mainForm').data('validator').settings.submitHandler = function (form) {
form.submit();
$submitButton.prop('disabled', true);
};
An alternative to disabling the submit button is to show an overlay in front of the form during submission to 1) block any further interaction with the form and 2) communicate that the page is "doing something." See this article for more detail.
Server-Side
I started off with Jim Yarbro's great answer above, but then I noticed Mark Butler's answer pointing out how Jim's method fails if someone submits forms via multiple browser tabs (because each tab has a different token and posts from different tabs can be interlaced). I confirmed that such a problem really does exist and then decided to upgrade from tracking just the last token to tracking the last x tokens.
To facilitate that, I made a couple of helper classes: one for storing the last x tokens and one for making it easy to store/retrieve objects to/from session storage. The main code now checks that the current token is not found in the token history. Other than that, the code is pretty much the same. I just made some little tweaks to suit my tastes. I included both the regular and asynchronous versions. The full code is below, but these are the critical lines:
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
}
Sadly, the fatal flaw of this approach is that the feedback from the first post (before any duplicates) gets lost. A better (but much more complex) solution would be to store the result of each unique request by GUID, and then handle duplicate requests by not only skipping doing the work again but also returning the same result from the first request, giving the user a seamless experience. This thorough article detailing Air BnB's methods of avoiding duplicate payments will give you an idea of the concepts.
PreventDuplicateFormSubmissionAttribute.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
// This class provides an attribute for controller actions that flags duplicate form submissions
// by adding a model error if the request's verification token has already been seen on a prior
// form submission.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateFormSubmissionAttribute: ActionFilterAttribute
{
const string TokenKey = "__RequestVerificationToken";
const string HistoryKey = "RequestVerificationTokenHistory";
const int HistoryCapacity = 5;
const string DuplicateSubmissionErrorMessage =
"Your request was received more than once (either due to a temporary problem with the network or a " +
"double button press). Any submissions after the first one have been rejected, but the status of the " +
"first one is unclear. It may or may not have succeeded. Please check elsewhere to verify that your " +
"request had the intended effect. You may need to resubmit it.";
public override void OnActionExecuting(ActionExecutingContext context)
{
HttpRequest request = context.HttpContext.Request;
if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
{
string token = request.Form[TokenKey].ToString();
ISession session = context.HttpContext.Session;
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
session.Put(HistoryKey, history);
}
}
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
HttpRequest request = context.HttpContext.Request;
if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
{
string token = request.Form[TokenKey].ToString();
ISession session = context.HttpContext.Session;
await session.LoadAsync();
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
session.Put(HistoryKey, history);
await session.CommitAsync();
}
await next();
}
}
}
RotatingHistory.cs
using System.Linq;
// This class stores the last x items in an array. Adding a new item overwrites the oldest item
// if there is no more empty space. For the purpose of being JSON-serializable, its data is
// stored via public properties and it has a parameterless constructor.
public class RotatingHistory<T>
{
public T[] Items { get; set; }
public int Index { get; set; }
public RotatingHistory() {}
public RotatingHistory(int capacity)
{
Items = new T[capacity];
}
public void Add(T item)
{
Items[Index] = item;
Index = ++Index % Items.Length;
}
public bool Contains(T item)
{
return Items.Contains(item);
}
}
SessonExtensions.cs
using System.Text.Json;
using Microsoft.AspNetCore.Http;
// This class is for storing (serializable) objects in session storage and retrieving them from it.
public static class SessonExtensions
{
public static void Put<T>(this ISession session, string key, T value) where T : class
{
session.SetString(key, JsonSerializer.Serialize(value));
}
public static T Get<T>(this ISession session, string key) where T : class
{
string s = session.GetString(key);
return s == null ? null : JsonSerializer.Deserialize<T>(s);
}
}
You could include a hidden (random or counter) value in the form post, a controller could track these values in an 'open' list or something similar; every time your controller hands out a form it embeds a value, which it tracks allowing one post use of it.
In its self, no, however depending on what the controller is actually doing, you should be able to work out a way.
Is a record being created in the database that you can check for to see if they've already submitted the form?
Just add this code at the end of your page. I am using "jquery-3.3.1.min.js" and "bootstrap 4.3.1"
<script type="text/javascript">
$('form').submit(function () {
if ($(this).valid()) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
</script>
Use the Post/Redirect/Get design pattern.
PS:
It looks to me that the answer by Jim Yarbro could have a fundamental flaw in that the __RequestVerificationToken stored in the HttpContext.Current.Session["LastProcessedToken"] will be replaced when a second form is submitted (from say another browser window). At this point, it is possible to re-submit the first form without it being recognized as a duplicate submission. For the proposed model to work, wouldn’t a history of __RequestVerificationToken be required? This doesn't seem feasible.
Dont reinvent the wheel :)
Use the Post/Redirect/Get design pattern.
Here you can find a question and an answer giving some suggestions on how to implement it in ASP.NET MVC.
You can also pass some sort of token in a hidden field and validate this in the controller.
Or you work with redirects after submitting values. But this get's difficult if you take heavily advantage of ajax.
This works on every browser
document.onkeydown = function () {
switch (event.keyCode) {
case 116: //F5 button
event.returnValue = false;
event.keyCode = 0;
return false;
case 82: //R button
if (event.ctrlKey) {
event.returnValue = false;
event.keyCode = 0;
return false;
}
}
}
You can do this by creating some sort of static entry flag that is user specific, or specific to whatever way you want to protect the resource. I use a ConcurrentDictionary to track entrance. The key is basically the name of the resource I'm protecting combined with the User ID. The trick is figuring out how to block the request when you know it's currently processing.
public async Task<ActionResult> SlowAction()
{
if(!CanEnterResource(nameof(SlowAction)) return new HttpStatusCodeResult(204);
try
{
// Do slow process
return new SlowProcessActionResult();
}
finally
{
ExitedResource(nameof(SlowAction));
}
}
Returning a 204 is a response to the double-click request that will do nothing on the browser side. When the slow process is done, the browser will receive the correct response for the original request and act accordingly.
Use this simple jquery input field and will work awesomely even if you have multiple submit buttons in a single form.
$('input[type=submit]').click(function () {
var clickedBtn = $(this)
setTimeout(function () {
clickedBtn.attr('disabled', 'disabled');
}, 1);
});

Resources