I want to protect our login actions by AntiforgeryToken attribute - I know why the exception from the topic occurs, however I can't seem to find any good solution for it.
Let say we have the following situations:
It's 8:00 AM, application users are coming to work, they sit down and starting the login process - right now it is very possible that some of the users will get the same ValidationToken. After the first one logs in - all other will see the above exception (or some other custom exception screen) when they attempt to login.
Some user logged in, then accidentally pressed the "back" button and attempted to log in again - while this is more unlikely, it can happen, and I don't want users to see exceptions when it does.
So the question is simple - how to prevent the above situations, or how to handle them so that users won't notice anything. I have tried the following:
Right now I was thinking to manually validate the token in action body, catch the error, and check if attempt was made by anonymous user:
public ActionResult SomeAction() { try { AntiForgery.Validate(); } catch(HttpAntiForgeryException ex) { if(String.IsNullOrEmpty(HttpContext.User.Identity.Name)) { throw; } } //Rest of action body here //.. //.. }
The above seems to prevent the errors - but is it safe? What alternatives are there?
Thanks in advance.
Best regards.
EDIT:
The final "solution" was to disable token validation on login form - there may be a better way to handle it, but it seems that all solutions I found, were ugly workarounds similar to mine proposed above.
Since there is no way to know how "safe" those alternatives are (if they are safe at all), we decided to do disable token validation on login.
The feature doesn't prevent any other type of data forgery or tampering based attacks. To use it, decorate the action method or controller with the ValidateAntiForgeryToken attribute and place a call to @Html. AntiForgeryToken() in the forms posting to the method.
AntiForgeryToken()Generates a hidden form field (anti-forgery token) that is validated when the form is submitted.
In order to prevent CSRF in ASP.NET, anti-forgery tokens (also known as request verification tokens) must be utilized. These tokens are randomly-generated values included in any form/request that warrants protection. Note that this value should be unique for every session.
Try setting (in global.cs):
AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.NameIdentifier;
This will add the name identifier to your token,
As for the double login issue try to use a script to document the date and time of the original submit to stop a second submit with the same token.
// jQuery plugin to prevent double submission of forms jQuery.fn.preventDoubleSubmission = function() { $(this).on('submit',function(e){ var $form = $(this); if ($form.data('submitted') === true) { // Previously submitted - don't submit again e.preventDefault(); } else { // Mark it so that the next submit can be ignored $form.data('submitted', true); } }); // Keep chainability return this; };
So we know one thing; users like the back button and have a habit of the double click, this is a big issue with AntiforgeryToken.
But depending on what your application does there are ways to limit their compulsion to do so. The simplest of which is to do your best to try and make the visitor not feel like they need to “rewind” their request to alter it.
Ensure that form error messaging is clear and concise to ensure the user knows what is wrong. Contexual errors give bonus points.
Always maintain form state between form submissions. Apart from passwords or credit card numbers, there’s no excuse thanks the to MVC form helpers.
@Html.LabelFor(x => x.FirstName)
If forms are spread across tabs or hidden divs such as those used in SPA frameworks like Angular or ember.js, be smart and show the controller layouts or form that the errors actually originated from in the form submission when displaying the error. Don’t just direct them to the home controller or first tab.
“What’s going on?” - Keeping the user informed
When a AntiForgeryToken doesn’t validate your website will throw an Exception of type System.Web.Mvc.HttpAntiForgeryException.
If you’ve set up correctly you’ve got friendly errors turned on and this will mean your error page will not show an Exception and show a nice error page that tells them what is up.
You can make this a little easier by at least giving the user a more informative page targeted at these exceptions by catching the HttpAntiForgeryException.
private void Application_Error(object sender, EventArgs e) { Exception ex = Server.GetLastError(); if (ex is HttpAntiForgeryException) { Response.Clear(); Server.ClearError(); //make sure you log the exception first Response.Redirect("/error/antiforgery", true); } }
and your /error/antiforgery
view can tell them Sorry you have tried to submit the same information twice
Another Idea is to log the error and return the user to the login screen:
Create a HandleAntiforgeryTokenErrorAttribute
class that Overrides the OnException method.
HandleAntiforgeryTokenErrorAttribute.cs:
public class HandleAntiforgeryTokenErrorAttribute : HandleErrorAttribute { public override void OnException(ExceptionContext filterContext) { filterContext.ExceptionHandled = true; filterContext.Result = new RedirectToRouteResult( new RouteValueDictionary(new { action = "Login", controller = "Account" })); } }
Global Filter:
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new HandleAntiforgeryTokenErrorAttribute() { ExceptionType = typeof(HttpAntiForgeryException) } ); } }
I would also use a few tools to record all your information as login is critical part of your application
NLog for general logging and emails on critical application exceptions (including web exceptions).
Elmah for filtering and email of web exceptions.
EDIT: Also you may want to look at a jQuery plugin called SafeForm. Link
EDIT:
I have seen allot of debate on this and everyone's views on the subject have valid points, How i look at it is (Taken from owasp.org)
Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they're currently authenticated, CSRF attacks specifically target state-changing requests, not theft of data. The anti-forgery token is specific to 'who is logged on'. So once you login, then go back, the old token is no longer valid
Now i also use authorized IP addresses for login to allot of my applications with 2 factor authorization if the users IP address changes, so if Cross-Site Request Forgery was in play the user wouldn't match the IP address and request 2 factor authorization. almost like the way a security router would work. but if you want to keep it on your login page i don't see a problem as long as you have your friendly error pages set up people will not get upset as they will see they did something wrong.
I just wanted to add to the pool pro's answer - regarding the Filter part - that one must be careful since that will override all the exceptions about AntiforgeryToken, and if you (like myself) have this token validation on other parts of your application you might consider adding some validation to the filter:
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new HandleAntiforgeryTokenErrorAttribute() { ExceptionType = typeof(HttpAntiForgeryException) }); } } public class HandleAntiforgeryTokenErrorAttribute : HandleErrorAttribute { public override void OnException(ExceptionContext filterContext) { string actionName = filterContext.Controller.ControllerContext.RouteData.Values["action"].ToString(); string controllerName = filterContext.Controller.ControllerContext.RouteData.Values["controller"].ToString(); if (actionName.ToLower() == "login" && controllerName.ToLower() == "account") { //Handle Error //In here you handle the error, either by logging, adding notifications, etc... //Handle Exception filterContext.ExceptionHandled = true; filterContext.Result = new RedirectToRouteResult( new RouteValueDictionary(new { action = "Login", controller = "Account" })); } else { base.OnException(filterContext); } } }
Note that I separate the cases where this exception is thrown on the Account/Login
method, from all the other mehtods where a [ValidateAntiForgeryToken]
might be used.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With