During the weekend I decided to try out a few strategies for protecting content against anonymous users. During this I got the chance to explore creating custom attributes as well as gaining a better understanding of how routing really works. Ultimately I came up with a very decent solution but not the most desirable in my mind.
First Attempt: Custom Attribute
For the first attempt I took the standard ActionFilterAttribute and derived from it to create the following attribute class:
public class IsAuthenticatedAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
string redirectPath = FormsAuthentication.LoginUrl;
HttpContext.Current.Response.Redirect(redirectPath, true);
}
}
}
This is a very rudimentary example of deriving from the ActionFilter attribute, I have seen examples where a custom attribute is used for logging and other reporting features. What happens in this example is we are override the OnActionExecuting method from the base class, so before the method this is decorating is executed this code will execute. As you can see, if the user is not authenticated we redirect to the login url specified in the web.config.
This is not a bad strategy, and is the strategy I am using at present within my application. However, it was not the approach I was looking for, mainly because I still have to remember to put the attribute on the methods. What I really would like is a single place that enforces the requirement that a user must be logged in for certain action to take place. Furthermore, I would like to be able to split up a controller’s functionality for its admin and non admin actions.
Second Attempt: Routing
Based on my requirements, I naturally decided to look at routing as a means to prevent access to a particular path (aka namespace). To begin, I created a directory under controllers (an Area as the term is known in the MVC world) called Admin and a controller called Series here to compliment the Series controller in the parent directory.
My first attempt was to institute a specific route for the admin actions and apply a constraint to the route that requires the user to be logged in, if they wish to access it, below is the following code I created:
routes.MapRoute(
"Admin",
"Admin/{controller}/{action}/{id}",
new { controller = "Series", action = "Index", id = 0 },
new {
isLoggedIn = new AuthenticatedPathConstraint()
},
new string[] {
"AnimeManager.Controllers.Admin"
}
);
The fourth parameter to MapRoute is a listing of the constraints to apply to this route. Constraints are classes that implement IRouteConstraint and are passed in via an arbitrary property name in an anonymous class:
.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }
public class AuthenticatedPathConstraint : IRouteConstraint
{
#region IRouteConstraint Members
public bool Match(HttpContextBase httpContext, Route route,
string parameterName, RouteValueDictionary values,
RouteDirection routeDirection)
{
if (routeDirection != RouteDirection.UrlGeneration)
{
if (!httpContext.User.Identity.IsAuthenticated)
{
httpContext.Response.Redirect(
FormsAuthentication.LoginUrl);
return false;
}
return true;
}
return true;
}
#endregion
}
The next parameter is a set of strings representing namespaces to search. When the MVC framework looks for controllers it, by default, re-curses through the Controllers directory and makes a string list of all controller names. Thus if you have controllers with the same name, even in a different space, you will get a server error with respect to the inherent ambiguity created. The strings provided to the fifth parameter of MapRoute serves to restrict the namespaces where MVC will look for potential controllers.
Now, I want to return to the constraint to explain a small, but critical, piece to this. The comparison against the RouteDirection enum is very important. Using ActionLink from the Html helper will invoke the routing mechanism as well as accessing a particular Url. Because of the way we are doing the redirection we want to allow UrlGeneration to occur regardless of state. To clarify, if we dont have this logic, when generating the link the page will never load and redirect endlessly.
To actually properly use this feature we need to use the GenerateLink Html Helper extension method as such:
<%= Html.GenerateLink( "test", "Admin", "Index", "Series", new RouteValueDictionary(new { id = 12 }), null) %>
Using GenerateLink we can actually describe which route we want to use to generate the final link. Remember anytime we call ActionLink we invoke the underlying routing system to generate the final URL, the same is true for GenerateLink, except it allows to tell what path we wish to use in the routing table. Because of this, I decided to keep a Default route map and place all subsequent routes following and refer to them by name as needed.
Conclusion
Both strategies do a good job in minimizing the amount of code needed to protect functionality and consolidating the logic to determine authentication status to a single place. I personally prefer the level of control I can get using Attributes over routing, granted the routing system still need a bit more work and research, I think it could be a very acceptable method in the future.