The odd thing I’ve found with the AuthorizeAttribute
, is how it redirects you to the sign in page, even when authenticated with a role that doesn’t have access to the controller or action. As you can imagine, Joe Blogs (the basic user) might feel a tad confused, when presented with a sign in page when he’s already signed in.
Wouldn’t it be kinder to tell Joe why he’s not special, and can’t view that super duper page meant for the powers that be?
Why the Authorize attribute always redirects to the sign in page?
If you open up the AuthorizeAttribute
with .NET Reflector, or even easier, look at the source code of ASP.NET MVC 5 on GitHub, you will see three possible conditions that cause the attribute to return a HttpUnauthorizedResult
.
I have provided a snippet of the code below with comments pointing out each condition:
protected virtual bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
// Condition 1: User is not authenticated
IPrincipal user = httpContext.User;
if (!user.Identity.IsAuthenticated)
{
return false;
}
// Condition 2: User is authenticated but does not have the correct name
if (_usersSplit.Length > 0 && !_usersSplit.Contains(user.Identity.Name,
StringComparer.OrdinalIgnoreCase))
{
return false;
}
// Condition 3: User is authenticated but does not have the correct role
if (_rolesSplit.Length > 0 && !_rolesSplit.Any(user.IsInRole))
{
return false;
}
return true;
}
All three conditions return false which makes the OnAuthorization
method call HandleUnauthorizedRequest
.
protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.Result = new HttpUnauthorizedResult();
}
This method sets the action methods result to a new instance of HttpUnauthorizedResult
. This sets the status code to 401 Unauthorized, causing the forms authentication module to redirect the user to the sign in page. In my opinion, it’s better to return a 403 Forbidden status code for conditions 2 and 3. There is a very simple way of doing this, which I will explain below.
CustomAuthorizeAttribute that returns HttpForbiddenResult
The first step is to create a new class called HttpForbiddenResult
which is used by the the custom attribute to set the status code to 403 forbidden;
public class HttpForbiddenResult : HttpStatusCodeResult
{
public HttpForbiddenResult()
: this(null)
{
}
public HttpForbiddenResult(string statusDescription)
: base(HttpStatusCode.Forbidden, statusDescription)
{
}
}
Next we need to create the CustomAuthorizeAttribute
and override the HandleUnauthorizedRequest
method so that it looks like the following:
public class CusomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new HttpForbiddenResult();
}
else
{
filterContext.Result = new HttpUnauthorizedResult();
}
}
}
As you can see from the code above, if the user is authenticated but does not belong to the role applied to the attribute, the HttpForbiddenResult
will be applied to the AuthorizationContext.Result
instead of the HttpUnauthorizedResult
.
How to display the error page
If you apply the CustomAuthorizeAttribute
to an action, and trigger a 403 forbidden response, you will see the following page is displayed:
This is still not a very helpful message, so to get around this we need to modify the Web.config
so that it loads an action from an ErrorController
.
<system.webServer>
<httpErrors errorMode="Custom" existingResponse="Replace">
<remove statusCode="403" />
<error statusCode="403" responseMode="ExecuteURL" path="/error/forbidden" />
</httpErrors>
</system.webServer>
Now all you need to do is make sure there is an ErrorController
with a Forbidden
action and a View
that displays a useful error message.
Joe is gonna be so pleased 🙂