I'm using the cookie middleware in ASP.NET Core 1.0 without ASP.NET Identity - as described in this article:
https://docs.asp.net/en/latest/security/authentication/cookie.html
When a user makes certain changes to his/her profile, I need to change some values in the cookie. In such scenarios, this article tells me to
call context.ReplacePrincipal() and set the context.ShouldRenew flag
to true
How exactly do I do that? I think the article is referring to HttpContext. I don't see a ReplacePrincipal() method under HttpContext.
I'd appreciate some help with this. Thanks.
In the article they are referencing the CookieValidatePrincipalContext from the OnValidatePrincipal delegate in the CookieAuthenticationEvents options.
You have to wire it up in the app.UseCookieAuthentication function in startup.cs like so:
app.UseCookieAuthentication(options =>
{
//other options here
options.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = UpdateValidator.ValidateAsync
};
});
And the UpdateValidator function would look like:
public static class UpdateValidator
{
public static async Task ValidateAsync(CookieValidatePrincipalContext context)
{
//check for changes to profile here
//build new claims pricipal.
var newprincipal = new System.Security.Claims.ClaimsPrincipal();
// set and renew
context.ReplacePrincipal(newprincipal);
context.ShouldRenew = true;
}
}
There is a good example in the SecurityStampValidator class which you can find on github: https://github.com/aspnet/Identity/blob/dev/src/Identity/SecurityStampValidator.cs
Related
I have the following code inside Startup - ConfigureServices:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = new PathString("/en/Authentication/LogIn");
});
Everything works great, but I cannot find a way to make LoginPath being localisable using URL parameter (en/de/es etc.)
My MapControllerRoute looks like:
"{lang}/{controller=Home}/{action=Index}/{id?}"
Is it possible to redirect to appropriate lang for authentication like if user was accessing /de/NeedAuth/Index - it should be redirected to /de/Authentication/LogIn ?
Ok. I spent an hour and here is a solution - in case anyone would have similar use case.
Step 1:
Creating a new class that would dynamically get current http requests to determine redirection:
public class CustomCookieAuthenticationEvents : CookieAuthenticationEvents
{
public override Task RedirectToLogin(RedirectContext<CookieAuthenticationOptions> context)
{
var httpContext = context.HttpContext;
var routePrefix = httpContext.GetRouteValue("lang");
context.RedirectUri = $"/{routePrefix.ToString()}/Authentication/LogIn";
return base.RedirectToLogin(context);
}
}
Step 2:
In Startup modifying cookie authentication declaration that relates to redirecting to authentication page:
services.AddScoped<CustomCookieAuthenticationEvents>();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = new PathString("/Authentication/LogIn");
options.EventsType = typeof(CustomCookieAuthenticationEvents);
});
Pay attention to registering CustomCookieAuthenticationEvents as service above.
We have an asp.net mvc application which I'm porting to aspnet core mvc.
In the old solution authentication is done using Windows authentication.
On top of that we have an "activity based authentication" (like http://ryankirkman.com/2013/01/31/activity-based-authorization.html); a user is connected to roles and the roles are connected to rights. The users roles and corresponding rights is stored in a separate application that serves as authorization service for our application and handful of other systems.
A query to the authorization service api for the rights of user "Jon Doe" would get a response like this:
{
Email:"Jon.Doe#acme.com",
FirstName:"Jon",
LastName:"Doe",
Resources:
[
"CanAccessWebApplication",
"CanCopyAppointment",
"CanEditAppointment",
"CanEditContact",
"CanSaveContact"
...
]
Alias:"1234567",
UserId:"1234"
}
In our current application these rights are checked using attributes (that we have implemented our selves) on the controller methods:
public ContactController
{
[ActionUserAccess("CanSaveContact")]
public ActionResult SaveContact
{
...
}
}
The current legacy implementation of the ActionUserAccessAttribute filter looks like this:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class ActionUserAccessAttribute : ActionFilterAttribute
{
private readonly string _accessRight;
public ActionUserAccessAttribute(string accessRight)
{
_accessRight = accessRight;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
throw new InvalidOperationException("ActionUserAccessAttribute can not be used for controllers or actions configured for anonymous access");
}
base.OnActionExecuting(filterContext);
var securityService = ContainerResolver.Container.GetInstance<ISecurityService>();
var hasResource = securityService.HasAccess(_accessRight);
if (!hasResource)
{
filterContext.Result =
new HttpStatusCodeResult(
403,
string.Format(
"User {0} is not authorized to access the resource:'{1}' ",
filterContext.HttpContext.User.Identity.Name,
_accessRight));
}
}
}
}
Porting the attribute/filter to aspnetcore seems quite straightforward, but according to this answer https://stackoverflow.com/a/31465227/1257728 by "asp.net security person" #blowdart we shouldn't.
If not porting the custom filter to aspnetcore, what would be the best fit to implement here?
Maybe we could use the Role based authentication https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?
We could create a middleware that populates the users access rights from the authorization service api and flatten the rights and add them as ClaimTypes.Role to the users' ClaimsIdentity ? Then we would use on the method above like:
[Authorize(Roles = "CanSaveContact")]
public ActionResult Save()
The misfit of this approach is that this is not really about roles, but more about the access rights.
I've also looked at the Policy based authorization:
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies
Which could look like this in the controller:
[Authorize(Policy = "CanSaveContact")]
public ActionResult Save()
But as I read the code in microsoft's policy based example above I would then have to add all available access rights that exists in the security service api as policies in the ConfigureService method of the Startup class to be able to use them. I think seems awkward (pseudo code):
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
IEnumerable<string> allAccessRights = _securtiyService.GetAllAccessRights();
services.AddAuthorization(options =>
{
foreach(var accessRight in allAccessRights)
{
options.AddPolicy(accessRight, policy => policy.Requirements.Add(new AccessRightRequirement(accessRight));
}
});
services.AddSingleton<IAuthorizationHandler, AccessRightHandler>();
}
The AccessRightHandler would then be resposible to validate the access right for the user. Writing an AccessRightHandler is ok, but it seems unnecessary to have to add all the rights as policies.
What would be the best approach to implement this kind of authorization in our aspnetcore application?
Great question, and I think a number of people would have the same problem upgrading to ASP.NET Core.
Barry Dorrans (#blowdart) is absolutely correct, you shouldn't write your own custom authorize attributes - Authorization in ASP.NET Core has been greatly improved, and you can definitely mould it to your needs.
It would of course greatly depend on your current application, and what roles do you have, so I'll make some assumptions based on the snippets you provided above.
Before I start, I REALLY recommend you read through the new Authorization docs for ASP.NET Core, as well as Barry Dorran's Authorization workshop on GitHub. I highly recommend you go through the latter, and he has a .NET Core 2.0 branch there as well.
Depending how you want to implement it, you could either go with Claims based authorization, or go resource based.
Looking at your roles, it seems like Resource based auth could actually work great in your case!
For example:
Identify possible operations (the operation Name is to be picked up from your Resources):
public static class Operations
{
public static OperationAuthorizationRequirement Access = new OperationAuthorizationRequirement { Name = "Access" };
public static OperationAuthorizationRequirement Copy = new OperationAuthorizationRequirement { Name = "Copy" };
public static OperationAuthorizationRequirement Edit = new OperationAuthorizationRequirement { Name = "Edit" };
public static OperationAuthorizationRequirement Save = new OperationAuthorizationRequirement { Name = "Save" };
public static OperationAuthorizationRequirement Delete = new OperationAuthorizationRequirement { Name = "Delete" };
}
Create a base resource authorization handler:
public abstract class BaseResourceAuthorizationHandler<TResource> : AuthorizationHandler<OperationAuthorizationRequirement, TResource>
{
private readonly string _resourceType;
public BaseResourceAuthorizationHandler(string resourceType)
{
_resourceType = resourceType;
}
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement, TResource resource)
{
if (context.User.HasClaim("Resources", $"Can{requirement.Name}{_resourceType}"))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Implement specific resource based handlers. The resources are binding objects in your application to entities in your Resources. This class will be the glue between your current resource roles, the Operations, and the authorization system in ASP.NET Core. These can also be extended to add extra logic for any specific resource types/operations
For example, for Appointments:
public class AppointmentAuthorizationHandler : BaseResourceAuthorizationHandler<Appointment>
{
public AppointmentAuthorizationHandler() : base("Appointment") { }
}
Which you then register:
services.AddSingleton<IAuthorizationHandler, AppointmentAuthorizationHandler>();
Then in your controllers:
public class AppointmentsController : Controller
{
IAppointmentsRepository _appointmentsRepository;
IAuthorizationService _authorizationService;
public AppointmentsController(IAppointmentsRepository appointmentsRepository,
IAuthorizationService authorizationService)
{
_appointmentsRepository = appointmentsRepository;
_authorizationService = authorizationService;
}
public IActionResult Edit(int id)
{
var appointment = _appointmentsRepository.Get(id);
if (appointment == null)
{
return new NotFoundResult();
}
if (!(await _authorizationService.AuthorizeAsync(User, appointment, Operations.Edit)))
{
return new ChallengeResult();
}
return View(appointment);
}
}
You can also do the same in views, to check whether the user is allowed to see the Edit button, for example:
#using Microsoft.AspNetCore.Authorization
#model IEnumerable<Appointment>
#inject IAuthorizationService AuthorizationService
<h1>Document Library</h1>
#foreach (var appointment in Model)
{
if (await AuthorizationService.AuthorizeAsync(User, appointment, Operations.Edit))
{
<p>#Html.ActionLink("Appointment #" + appointment.Id, "Edit", new { id = appointment.Id })</p>
}
}
P.S. Just to add a note - yes, you lose the ability to filter by attributes, but in the end it's better this way. First and foremost - you move away from String based roles, you request permissions based on an operation type and resource type. Secondly, you can handle permissions in a much better (and intelligent way), as well as combine multiple permission checks.
It looks more complex, but it's also MUCH more powerful :)
Going to play the devil's advocate here, and suggest an alternative to my other answer - this could be a simpler option based on #mortb's request, and could fit some people that are migrating from their current systems.
Based on your situation, the Policy based auth really wouldn't fit your usecase - it's a more powerful option, you're not really using any of it, other than checking for the existence of a Resource string from your API.
On the other hand, I wouldn't discard the Roles approach. The resource list you get from the external API isn't strictly resources, but at the same time it maps quite perfectly to your needs. At the end of the day, all you're trying to do is to check whether the user has one (or more) Resource access permissions for a specific request.
Like you mentioned on your post, you'd have to extend your authorization to populate the roles from your external API. Don't forget that your ClaimsIdentity has a RoleClaimType property, which marks the type of the claim used to store the roles. It'll usually be set to ClaimTypes.Role, but not always.
You could even go ahead, and create custom auth attributes, not unlike this:
public class AuthorizeAccessAttribute : AuthorizeAttribute
{
public AuthorizeAccessAttribute(string entity)
{
Roles = "CanAccess" + entity;
}
}
public class AuthorizeEditAttribute : AuthorizeAttribute
{
public AuthorizeEditAttribute(string entity)
{
Roles = "CanEdit" + entity;
}
}
So you could use it as follows:
[AuthorizeEdit("Appointment")]
public IActionResult Edit(int id)
{
return View();
}
I have an MVC application and a custom class called AuthorisationFilter which has a .NET interface of IAuthorizationFilter, this has an OnAuthorization method which gets called when I click around my site, at that point I go about validating the security access of the user (which works), but I don't want to do this all the time as it is time consuming.
In this I'm trying to use the Session to store a temporary piece of login information (this is an internal application by the way), but I can't get it working as I'd expect. I can't just use an HttpContext so end up constantly creating a new instance of HttpContextBase, which I assume is then clearing out the Session. My code is as follows:
internal void SetSecurityLevel(int token)
{
HttpContextBase _cBase = new HttpContextWrapper(HttpContext.Current);
_cBase.Session["SecurityRights"] = token;
}
internal int GetSecurityLevel()
{
HttpContextBase _cBase = new HttpContextWrapper(HttpContext.Current);
if (_cBase.Session["SecurityRights"] == null)
{
SetSecurityLevel(-1);
}
return (int)_cBase.Session["SecurityRights"];
}
Please note this is only part of the code, SetSecurityLevel is set to the correct value by a separate method call which is not shown
Anyway what I'm really wanting to do is have the session set in this class and have it persisted. I tried a few different ways, including setting the context when the class is initialised, but I end up with a NullReference on the .Session object in GetSecurityLevel
private HttpContextBase _cBase = new HttpContextWrapper(HttpContext.Current);
public AuthorisationFilter()
{
_cBase = new HttpContextWrapper(HttpContext.Current);
}
Is there a way I can do this within the class?
You are right that you can't set in the constructor but you can in OnActionExecuting when the context is available
public override void OnActionExecuting(ActionExecutedContext filterContext)
{
_session = = filterContext.HttpContext.Session;
I would wonder a little why you're setting this security level in the filter and not just doing it directly from where it's needed.
I am new in unit test and MVC development.
I have a question for using moq for unit testing in asp.net mvc. I have a controller which accepts an ajax action:
[HttpPost,Authorize]
public ActionResult GrabLink()
{
string username = HttpContext.User.Identity.Name;
string rssUrl = Request.Params["Grablink"].ToString();
...}
This action deals with the http request which I generate from the view:
var mockRequest = new Moq.Mock<HttpRequestBase>();
but I can not find a way to define the parameters I used in the class. Also, is there any way to use the value binding provider directly to pass the value to the controller if I would like to do an ajax post?
I am a newbie in handling web request. If you have some good tutorial for better understanding the Http request (as well as the Httpcontext and related classes in asp.net) please post here. Thank you very much!
This works very well for me:
var controller = new HomeController();
var context = new Mock<HttpContextBase>(MockBehavior.Strict);
var controllerContext = new Mock<ControllerContext>();
controllerContext.SetupGet(x => x.HttpContext.User.Identity.Name)
.Returns("TestUser");
controllerContext.SetupGet(x => x.HttpContext.User.Identity.IsAuthenticated)
.Returns(true);
controllerContext.SetupGet(x => x.HttpContext.Request.IsAuthenticated)
.Returns(true);
controller.ControllerContext = controllerContext.Object;
// As a bonus, instantiate the Url helper to allow creating links
controller.Url = new UrlHelper(
new RequestContext(context.Object, new RouteData()), new RouteCollection());
This will allow you to initialize any user you want as an authenticated user, and the last line will allow you to user the Url helper within the controller even though you're calling it from a unit test.
As Scott said HttpContext makes Controllers hard to test. Anyway he's got a pretty solution at here.
BTW why didn't you make rssUrl a parameter if it is assigning by POST or GET?
e.g.
//POST: /GrabLink?rssUrl=bla bla...
[HttpPost,Authorize]
public ActionResult GrabLink(IPrincipal user, string rssUrl) {
string userName = user.Name;
}
Ok, #cem covered your second question very well.
For your first, nerddinner, and If I'm not mistaken, when you create a new Internet Application with Unit test, in Visual Studio, have the following mock classes for HttpContext. Its at the bottom of this file.
You could use these (or create a new Internet App +Tests with VS) and copy all the fake classes for your tests. (I wrote a Moq example below)
It looks like this:
public class MockHttpContext : HttpContextBase {
private IPrincipal _user;
public override IPrincipal User {
get {
if (_user == null) {
_user = new MockPrincipal();
}
return _user;
}
set {
_user = value;
}
}
public override HttpResponseBase Response
{
get
{
return new MockHttpResponse();
}
}
}
public class MockHttpResponse : HttpResponseBase {
public override HttpCookieCollection Cookies
{
get
{
return new HttpCookieCollection();
}
}
}
Not tested, but to Use mock it would look like this:
var fakeReqBase = new Mock<HttpRequestBase>();
fakeReqBase.Setup(f => f.User).Returns(new GenericIdentity("FakeUser"));
//generic identity implements IIdentity
fakeUserRepo.Object;//this returns fake object of type HttpRequestBase
Checkout the Moq Quickstart. Its quite easy to get used to, and the fluent interface really helps.
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);
});