Introduction
Applying security and redirection to a view when a user cannot access a controller or a controller action in MVC. I chose the following solution because using integrated security with ASP.NET user / privileges is the easiest way to secure an application. Moreover, it fits very well with the third party tools as Telerik. For example, the Telerik menu automatically adjusts without a line of code by the simple fact that a user does not have the role to access a controller or action.
Using the code
The easiest and flexible way I found is to create me a new attribute inherited from the System.Web.Mvc.AuthorizeAttribute
class. There, I add three properties, ActionOrViewName, Controller and Area. You can set a viewname with or without an area il you have a shared view at the root or in a specific area. Or, you can set an action and a controller, with or without an area to redirect to action. I modify the behavior of the method HandleUnauthorizedRequest
, as follow :
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class AuthorizeWithRedirectToViewAttribute : AuthorizeAttribute
{
#region Private Fields
private const string DefaultActionOrViewName = "UnauthorizedAccess";
private string _actionOrViewName;
#endregion
#region Properties
public string ActionOrViewName
{
get
{
return string.IsNullOrWhiteSpace(_actionOrViewName)
? DefaultActionOrViewName
: _actionOrViewName;
}
set { _actionOrViewName = value; }
}
public string Controller { get; set; }
public string Area { get; set; }
#endregion
#region Overrides
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.IsChildAction)
base.HandleUnauthorizedRequest(filterContext);
else
{
var factory = new HttpUnauthorizedWithRedirectToResultFactory();
filterContext.Result = factory.GetInstance(
Area,
Controller,
ActionOrViewName);
}
}
#endregion
}
This way, instead of get a blank page, I replace it by a customized Mvc
ActionResult
, witch inherits from System.Web.Mvc.HttpUnauthorizedResult
class. The new result will get all necessary parameters to render the view.
public abstract class HttpUnauthorizedWithRedirectToResultBase : HttpUnauthorizedResult
{
protected ActionResult _result;
public override void ExecuteResult(ControllerContext context)
{
if (context == null)
throw new ArgumentNullException("context");
if (context.HttpContext.Request.IsAuthenticated)
{
context.HttpContext.Response.StatusCode = 200;
InitializeResult(context);
_result.ExecuteResult(context);
}
else
base.ExecuteResult(context);
}
protected abstract void InitializeResult(ControllerContext context);
}
public class HttpUnauthorizedWithRedirectToViewResult
: HttpUnauthorizedWithRedirectToResultBase
{
#region Ctors
public HttpUnauthorizedWithRedirectToViewResult(string viewName, string area)
{
_viewName = string.IsNullOrWhiteSpace(viewName) ? viewName : viewName.Trim();
_area = string.IsNullOrWhiteSpace(area) ? area : area.Trim();
}
#endregion
#region Private Fields
private readonly string _area;
private readonly string _viewName;
#endregion
#region Overrides of HttpUnauthorizedWithRedirectToResultBase
protected override void InitializeResult(ControllerContext context)
{
SetAreaRouteData(context);
_result = new ViewResult
{
ViewName = _viewName,
};
}
#endregion
#region Methods
private void SetAreaRouteData(ControllerContext context)
{
if (context.RequestContext.RouteData.DataTokens.ContainsKey("area"))
{
if (!string.IsNullOrWhiteSpace(_area))
context.RequestContext.RouteData.DataTokens["area"] = _area;
}
else
context.RequestContext.RouteData.DataTokens.Add("area", _area);
}
#endregion
}
public class HttpUnauthorizedWithRedirectToRouteResult
: HttpUnauthorizedWithRedirectToResultBase
{
#region Ctors
public HttpUnauthorizedWithRedirectToRouteResult(string action, string controller, string area)
{
_action = string.IsNullOrWhiteSpace(action) ? action : action.Trim();
_controller = string.IsNullOrWhiteSpace(controller) ? controller : controller.Trim();
_area = string.IsNullOrWhiteSpace(area) ? area : area.Trim();
}
#endregion
#region Private Fields
private readonly string _action;
private readonly string _area;
private readonly string _controller;
#endregion
#region Overrides of HttpUnauthorizedWithRedirectToResultBase
protected override void InitializeResult(ControllerContext context)
{
_result = new RedirectToRouteResult(new RouteValueDictionary
{
{"area", _area},
{"controller", _controller},
{"action", _action}
});
}
#endregion
}
public class HttpUnauthorizedWithRedirectToResultFactory
{
public HttpUnauthorizedWithRedirectToResultBase GetInstance(string area,
string controller, string actionOrViewName)
{
if (string.IsNullOrWhiteSpace(actionOrViewName))
throw new ArgumentException("You must set an actionOrViewName");
if(string.IsNullOrWhiteSpace(controller) )
return new HttpUnauthorizedWithRedirectToViewResult(actionOrViewName, area);
return new HttpUnauthorizedWithRedirectToRouteResult(actionOrViewName, controller, area);
}
}
I modify the ExecuteResult
method behavior to get same reaction as a ViewResult
or an RedirectToRouteResult
.
In this case, the view of redirection does't receive any value, it shows only a generic message, but it can be more complex.
To do this, you simply have to add a model or use the view bag.
Why inherit from HttpUnauthorizedResult
class? Why not simply return a ViewResult in HandleUnauthorizedRequest
method?
Because I'm using Telerik third party, Telerik verify if the obtained result is an HttpUnauthorizedResult
to show or hide menu items.
It's important to verify the filterContext.IsChildAction
to preserve the expected result on child action. This way, when the user doesn't have permission,
MVC will simply hide child section.
In application, the attribute is placed either on the declaration of the class or action method.
[AuthorizeWithRedirectToView(Roles = "ReaderRole,WriterRole,SpecialRole")]
public class MyController : Controller
{
[AuthorizeWithRedirectToView(Roles = " ReaderRole , WriterRole , SpecialRole "),
Controller = "Security", Area = "MyArea")]
public ViewResult Details(Guid id)
{
…
}
[ChildActionOnly]
[AuthorizeWithRedirectToView(Roles = " SpecialRole ")]
public PartialViewResult ChildDetails(Guid id)
{
…
}
[AuthorizeWithRedirectToView(Roles = " WriterRole ", ActionOrViewName = "OtherUnauthorizedAccess")]
public ViewResult Details(ViewModel viewModel)
{
…
}
}
UnauthorizedAccess.cshtml
@{
ViewBag.Title = "Unauthorized Access";
}
<h2>Unauthorized access.</h2>
<p>
Sorry, you lack privileges to access this page. <a href="javascript:history.go(-1)">Click here</a> to go back.
</p>
In conclusion...
I hope this article has helped you or help you in the future. I adjust my solution to support
Asp.Net Role Based security on a Sql Compact database and run in stand-alone mode. Feel free if you have any suggestions or comments! You can download the full version of the project here.