ASP.NET’s built in CSRF (Cross-site request forgery) is pretty straight forward. You add a token to your views via an HTML Helper, and then decorate your controller actions with a specific attribute to validate the token on POST. There are many times, seemingly randomly, where users have invalid tokens on their requests. MVC throws a 500 error with an HttpAntiForgeryException. For legitimate users, this is not an optimal experience.
What can cause these sorts of exceptions? A non-trivial example involves simply having multiple browser windows open while unauthenticated, becoming authenticated in one window, and then returning to the previous window to perform actions. In the window, where we we never refreshed the page, our token is invalid. The user will see an exception when making a POST of the form or whatever it is that they were doing. In my case, this was an SSO login page.
Fortunately, there are many hooks, filters, attributes, etc that we can create in MVC to mitigate these types of exceptions. An error handling attribute to intercept these exceptions is below.
This attribute is attached to the global filters collection and handles all instances, produced by MVC controllers, of an HttpAntiForgeryException occurring. I have a bit of extra cruft to get the machine’s IP Address and perform logging. If you don’t have the “X-Forward-For” header available or don’t care to log the exception, ignore that piece of code.
public class HandleAntiForgeryExceptionAttribute : HandleErrorAttribute { private string _xforwardedFor = "X-Forwarded-For"; private IPAddress _ipAddress; [Inject] public ILog _log { get; set; } /// <summary> /// We want to handle these exceptions to provide the user a better experience. /// </summary> public HandleAntiForgeryExceptionAttribute() { this.ExceptionType = typeof(HttpAntiForgeryException); if (_log == null) { _log = (ILog)DependencyResolver.Current.GetService(typeof(ILog)); } } public override void OnException(ExceptionContext filterContext) { if (this.ExceptionType.IsAssignableFrom(filterContext.Exception.GetType())) { // See if we can determine the IP Address var headers = filterContext.HttpContext.Request.Headers; _ipAddress = string.IsNullOrWhiteSpace(headers[_xforwardedFor]) ? IPAddress.None : IPAddress.Parse(headers[_xforwardedFor]); var ipAddressMsg = _ipAddress == IPAddress.None ? "Unknown" : _ipAddress.ToString(); // Is the user logged in? var username = filterContext.HttpContext.User.Identity.IsAuthenticated ? filterContext.HttpContext.User.Identity.Name : "Annonymous"; // Generate our message and log it var message = string.Format("HttpAntiForgeryException occurred for user: {0}, IP Address: {1}", username, ipAddressMsg); _log.Info(message, filterContext.Exception); // Mark the exception handled and simply refresh the page. Alternatively, we could redirect to a specific // error view. filterContext.ExceptionHandled = true; filterContext.Result = new RedirectResult(filterContext.HttpContext.Request.RawUrl); } } }
To attach the filter, you only need to add it to the global filter collection:
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new ElmahHandleErrorAttribute()); filters.Add(new HandleAntiForgeryExceptionAttribute()); } }
Note that I have an Elmah handler too. Since I’m logging these exceptions in the HandleAntiForgeryExceptionAttribute, I added one line of code in my Elmah handler to ignore these exceptions for Db logging purposes:
private void SendToILog(Exception e) { // We won't log HttpAntiForgeryException to log4net since we have a special handler for that. if (e is HttpAntiForgeryException) return; _log.Error(e.Message, e); }
When the HttpAntiForgeryException occurs now, the page is simply refreshed and the error is logged. This is a better experience for the user rather than throw up a status code 500 error page.