How to handle 401 (Unauthorized), 403 (Forbidden) and 500 (Internal Server Error) in mvc. For ajax/non-ajax calls and under aspx forms authentication.
It can be altered to handle various uncaught exceptions differently and react differently whether the request is ajax or not. The auth part allows it to bypass any regular mvc web forms redirect-to-login-page and instead return 401 unauthorized - then your client-side js framework can react to http status 401/403 more easily.
// FilterConfig.cs:
filters.Add(new ApplicationAuthorizeAttribute());
filters.Add(new ApplicationHandleErrorAttribute());
public class ApplicationAuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Note: To reach here, a Web.config path-specific rule 'allow users="?"' is needed (otherwise it redirects to login)
var httpContext = filterContext.HttpContext;
var request = httpContext.Request;
var response = httpContext.Response;
if (request.IsAjaxRequest())
{
response.SuppressFormsAuthenticationRedirect = true;
response.TrySkipIisCustomErrors = true;
}
filterContext.Result = new HttpUnauthorizedResult();
}
}
public class ApplicationHandleErrorAttribute : HandleErrorAttribute
{
public override void OnException(ExceptionContext context)
{
var exception = context.Exception is AggregateException
? ((AggregateException)context.Exception).InnerExceptions.First()
: context.Exception;
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
var isAjax = request.IsAjaxRequest();
if (exception is MyCustomPermissionDeniedException)
{
filterContext.Result = new HttpStatusCodeResult(HttpStatusCode.Forbidden);
response.TrySkipIisCustomErrors = isAjax;
filterContext.ExceptionHandled = true;
return;
}
#if DEBUG
if (!isAjax)
{
// Show default aspx yellow error page for developers
return;
}
#endif
var requestUri = request.Url == null ? "" : request.Url.AbsoluteUri;
MyCustomerLogger.Log(exception, requestUri);
response.Clear();
response.StatusCode = (int)System.Net.HttpStatusCode.InternalServerError;
#if DEBUG
var errorMessage = exception.Message;
#else
var errorMessage = "An error occurred, please try again or contact the administrator.";
#endif
response.Write(isAjax
? JsonConvert.SerializeObject(new {Message = errorMessage})
: errorMessage);
response.End();
response.TrySkipIisCustomErrors = true;
context.ExceptionHandled = true;
}
}
Web.config:
<system.webServer>
<authentication mode="Forms">
<forms name=".MYAUTHCOOKIE" protection="All" loginUrl="/Account/Login" timeout="18000" slidingExpiration="true" enableCrossAppRedirects="false" />
</authentication>
<authorization>
<deny users="?" />
</authorization>
</system.webServer>
<!-- ajax api security done via ApplicationAuthorizeAttribute -->
<location path="api">
<system.web>
<authorization>
<allow users="?"/>
</authorization>
</system.web>
</location>
Additional route for web service api requests: (put above regular mvc route)
// This route has special ajax authentication handling (no redirect to login page)
routes.MapRoute(
name: "DefaultApi",
url: "api/{controller}/{action}/{id}",
defaults: new { id = UrlParameter.Optional }
);
Sample client side code for jquery to handle the error:
$.ajaxSetup({
complete: function onRequestCompleted(xhr, textStatus) {
if (xhr.readyState == 4 && xhr.status == 401) {
// Not needed with smart status: && xhr.responseText.substring(0, 150).indexOf("<title>Log in") != -1
//location.href = "/Account/Login";
alert("Your session has timed out.");
}
}
});
Alternatively, you could make all auth go through ApplicationHandleErrorAttribute, and get rid of that web.config deny users="?". But I have a legacy aspx page which doesn't hit the mvc filtering so I want that deny users="?".