Manage security and redirection for non authorized access in MVC






4.50/5 (7 votes)
How to apply security and redirection to a view when a user cannot access a controller or a controller action in MVC
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 :
/// <summary>
/// Use this class instead of Authorize to redirect to a spcific view on unauthorized access.
/// If this attribute is used on a child action, it does the base result else, it redirect
/// to the specified view. The default view is UnauthorizedAccess, but can be overriden with
/// the ActionOrViewName property.
/// </summary>
[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
/// <summary>
/// The name of the view to render on authorization failure. Default is
/// "UnauthorizedAccess".
/// </summary>
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
/// <summary>
/// Processes HTTP requests that fail authorization.
/// </summary>
/// <param name="filterContext"> Encapsulates the information for using
/// <see cref="T:System.Web.Mvc.AuthorizeAttribute" /> . The
/// <paramref name="filterContext" /> object contains the controller, HTTP context,
/// request context, action result, and route data. </param>
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)
{
…
}
// To define a view other than the default "UnauthorizedAccess".
[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.