|
Previous articles in this series:
- ASP.NET MVC Dev Series I - Using ASP.NET Membership and Role Providers w/ the MVC Framework
If you've done any secured ASP.NET application development in the past, then you're probably quite familiar with how to rig up URL authorization in your web.config file. Basically, if you're building a forms authentication system (e.g. the users have to supply credentials, they aren't pushed from the client via Windows) then you will undoubtedly have a couple of sections of your site that users can hit anonymously and some sections of the site that users can only hit when authenticated. Furthermore, if you're using a Role provider, you might even have some section of the site that is only available to users with the Admin role assigned to them.
While this stuff all technically works under the ASP.NET MVC framework, it doesn't work as elegantly as I would like. The problem I ran into is that the <location/> element in Web.config doesn't know anything about MVC. All ASP.NET is going to do on your behalf is examine in the incoming requested URL, compare it against the locations defined in Web.config, and determine whether the user has access to that location. If the user doesn't have access to that location, they are usually redirected to a login page.
What happens here is the very nature of the ASP.NET MVC allows me to make my controller routing paths (URLs) dynamic and flexible. It is entirely possible and perhaps even likely that I will be unable to accurately express my secured URL logic using simple hard-coded URLs in <location/> elements. What I really want to do is secure individual methods of my controllers and perhaps entire controllers. For example, if I am building an online banking application and I have a controller called AccountsController, I don't want users to be able to invoke methods on this controller unless they are authenticated. I don't care what URL they used - they shouldn't be able to invoke methods on the controller unless there is an authenticated principal in the context.
Fortunately, we've got some CAS (Code Access Security) attributes that we can decorate our controller classes with that work perfectly well within the context of any (including MVC) ASP.NET application. To ensure that our controller class is never invoked by an anonymous user, we can decorate it as follows:
[PrincipalPermission(SecurityAction.Demand, Authenticated=true)]
public class AccountsController : Controller
{ ... }
Now if we attempt to navigate to any of the URLs that route to this controller ASP.NET will check to make sure that the current principal is indeed authenticated. If I'm using forms authentication, its going to eventually examine my cookies to see if I've got the forms auth cookie stored. Securing my controller class this way allows me to tweak my routing without having to go back and tweak my web.config file. I also like being able to see the security status of each class and method while looking at the class and method. I thoroughly dislike having knowledge separated in multiple locations because that makes it easy for me to forget and neglect said knowledge in my code.
So what happens when you hit a secured controller without the proper credentials? Unfortunately, you're going to get an ASP.NET exception and the default behavior for that is just plain UGLY. There are two ways around this:
Write some code in the Global.asax.cs that checks to see if the root cause of the thrown exception was a security exception. If such is the case, re-direct to your application's login screen:
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError().GetBaseException();
if (ex is SecurityException)
{
Response.Redirect("/Login");
}
}
Unfortunately, inside the confines of Application_Error, the protection level on RedirectToAction won't let me invoke the method from outside the controller, so I'm stuck using a standard Response.Redirect() here. Since "/Login" is a pretty fixed and constant URL I'm not too worried. If I had multiple potential destinations when a security failure occurred, then I would be concerned about my lack of access to MVC routing logic here.
In this option, instead of decorating the classes with declarative permission demands, the code makes a check imperatively through method invocations. It basically looks something like this:
PrincipalPermission pp = new PrincipalPermission(User.Identity.Name, "Administrator", true);
pp.Demand();
These lines of code will demand that the current user is identified with the given name, is part of the Administrator role, and is authenticated. You would then wrap this in an exception handler and then do your conditional re-direct if the demand failed. While this is a little more explicit, it totally violates DRY (Don't Repeat Yourself). My code would be positively littered with nasty, repetetive security checks.
So for now, until I find something more elegant, I am using the first method. I get the elegance of neatly decorating my controller classes and individual methods with attributes (the attributes have a cleaner syntax than creating instances of the permissions because you have to union them manually for complex assertions) and I get one set of handler code that responds to the security failure.
Have you looked into ActionFilter attrbiutes, introduced in MVC Preview 2?
Give them a google, you should be able to do what you want with them :)
It's what I use.