Hanalei, Hawaii Monday, February 08, 2010

ASP.NET MVC: Securing Your Controller Actions

Many people on the forums want to know how to best protect Actions on their Controller using Forms Authentication. The MVC Team has done a nice job introducing Filters (using Attributes) to this latest drop of MVC, and in this post I'll show you how to create a filter that can handle security.

Many people on the forums want to know how to best protect Actions on their Controller using Forms Authentication. The MVC Team has done a nice job introducing Filters (using Attributes) to this latest drop of MVC, and in this post I'll show you how to create a filter that can handle security.

 

What's An Attirbute?
Attributes are "decorations" for classes and methods that essentially offer context or "shape" the class/method they are used on. You've probably seen them before and perhaps not known about them. One I use often is the FxCop suppression :) - which tells the compiler to leave me and my bad code alone:

[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "1000:YouShouldntBeCoding", MessageId = "1#", 
Justification="That's not very nice... but I'm used to it :p")] 

There's a lot out there on their use, and you can read more about them here.

The ActionFilterAttribute
ASP.NET MVC Preview 2 introduces the new ActionFilterAttribute, which allows you to declare "Filter Attributes" for the actions on your controller. Think of these things, in essence, as "OnLoad" events - a bit of code that's executed before or after your Action.

Using the ActionFilterAttribute is pretty simple. You declare a class an override one of two base methods:

Filter1

Here you can see we have two methods to override:

  • OnActionExecuting: called when the Action is called (useful for authentication for example)
  • OnActionExecuted: called when the Action has finished executing (useful for logging)

When you create your filter, you also get access to FilterExecutingContext - a nice object that tells you what's going on in the app around you:

Filter2

For those of you who aren't familiar with Attributes, you can also declare properties that can be set when the Attribute is used:

Filter3Prop

And you can set these properties like this:

 SettingMyFilter

 

 SettingMyFilter2

This passes the value "SomeSetting" into the property "MyFilterProperty" and you can use that within your OnActionExecuting method.

Putting It Together
What we want to do is check before our Action is executed whether the current user is logged in, and if they aren't we want to redirect them off to the login page, appending a Url that they can then return to.

Why use a filter for this and not the Web.Config as with Webforms? The reason is that you can't accurately lock down an Action using forms auth, as before. Phil wrote about this as well:

Suppose you have a website and you wish to block unauthenticated access to the admin folder. With a standard site, one way to do so would be to drop the following web.config file in the admin folder...

<?xml version="1.0"?>
<configuration>
    <system.web>
        
        <authorization>
            <deny users="*" />
        </authorization>

    </system.web>
</configuration>

Attempt to navigate to the admin directory and you get an access denied error. However, suppose you use a naive implementation of WebFormRouteHandler to map the URL fizzbucket to the admin dir like so...

RouteTable.Routes.Add(new Route("fizzbucket"
  , new WebFormRouteHandler("~/admin/secretpage.aspx"));

Now, a request for the URL /fizzbucket will display secretpage.aspx in the admin directory. This might be what you want all along. Then again, it might not be.

The point here is that Urls Do Not Map To Pages And Directories and There Is More Than One Way To Skin An Action - you can easily shoot yourself in the foot with your routes, and inadvertantly open up a Url to a secured page. Securing your Action is always preferable.

Given all of this, we can now work up our new filter to check for authentication - the RequiresAuthentication Filter:

    /// <summary>
    /// Checks the User's authentication using FormsAuthentication
    /// and redirects to the Login Url for the application on fail
    /// </summary>
    public class RequiresAuthenticationAttribute : ActionFilterAttribute {

        public override void OnActionExecuting(FilterExecutingContext filterContext) {
           
            //redirect if not authenticated
            if (!filterContext.HttpContext.User.Identity.IsAuthenticated) {
                
                //use the current url for the redirect
                string redirectOnSuccess = filterContext.HttpContext.Request.Url.AbsolutePath;

                //send them off to the login page
                string redirectUrl = string.Format("?ReturnUrl={0}", redirectOnSuccess);
                string loginUrl = FormsAuthentication.LoginUrl + redirectUrl;
                filterContext.HttpContext.Response.Redirect(loginUrl, true);

            }

        }
    }

Using this is pretty straightforward - just attach it to your method that you want secured:

finished

 

Another useful filter would be to check for a given Role:

    /// <summary>
    /// Checks the User's role using FormsAuthentication
    /// and throws and UnauthorizedAccessException if not authorized
    /// </summary>
    public class RequiresRoleAttribute : ActionFilterAttribute {
        
        public string RoleToCheckFor { get; set; }

        public override void OnActionExecuting(FilterExecutingContext filterContext) {
            //redirect if the user is not authenticated
            if (!String.IsNullOrEmpty(RoleToCheckFor)) {
                
                if (!filterContext.HttpContext.User.Identity.IsAuthenticated) {

                    //use the current url for the redirect
                    string redirectOnSuccess = filterContext.HttpContext.Request.Url.AbsolutePath;

                    //send them off to the login page
                    string redirectUrl = string.Format("?ReturnUrl={0}", redirectOnSuccess);
                    string loginUrl = FormsAuthentication.LoginUrl + redirectUrl;
                    filterContext.HttpContext.Response.Redirect(loginUrl, true);

                } else {
                    bool isAuthorized = filterContext.HttpContext.User.IsInRole(this.RoleToCheckFor);
                    if (!isAuthorized)
                        throw new UnauthorizedAccessException("You are not authorized to view this page");
                }
            } else {
                throw new InvalidOperationException("No Role Specified");
            }
        }
    }

Note here that I'm passing in a string to check for the role:

AdminCheck

You can change this to be an enum, constant, or whatever works. You can also change it up to be more behavioral - such as "RequiresUserCanEditPage" and in this method check to be sure they are "Administrators" or "Content Editors". The point is that it's up to you - and that's as it should be.

Technorati Tags:


Jab - March 12, 2008 - Good stuff, Rob. Really appreciate all the support from the MVC guys.
Craig - March 12, 2008 - Sweet. I was just about to implement this after reading a few other examples, but it looks like you have done it for me :-)
taq - March 12, 2008 - That's really helpful, thanks! One question though, which will probably show how much of a novice I am:

What sets the value for FormsAuthentication.LoginUrl? I was under the impression that this was read from the web.config's authentication section. It appears though that this is not the case.

Regardless, these filters provide a lot of utility.
Haacked - March 12, 2008 - Man, I love those colors in the code samples. Who did that VS theme anyways? ;)
Rasmus - March 12, 2008 - Nice work, but instead of redirecting to the login page from the filter why not just do if (!filterContext.HttpContext.User.Identity.IsAuthenticated) { filterContext.HttpContext.Response.StatusCode = 401 filterContext.Cancel = true }
Rob Conery - March 12, 2008 - @taq - when you setup Forms Auth in the web.config it asks you where the user is supposed to log in (loginUrl) - this gets mapped to FormsAuthentication.LoginUrl. @Rasmus - you can do that but it kills the idea that the user is a real user. If the request just dies you're not communicating much.
Rasmus - March 12, 2008 - @Rob Actually the asp.net runtime (I think), will do the redirect to the login page for you, if the FormsAuthentication is set up in the web.config
Denny Ferrassoli - March 12, 2008 - This is really neat stuff Rob. Thank you for sharing!
tgmdbm - March 12, 2008 - Yeah. Although, it probably makes more sense to Redirect to a controller action, instead of a Url. Also, I would make RequiresAuthenticationAttribute throw a SecurityException, similarly, RequiresRoleAttribute would throw the same exception if you weren't authenticated. Then I'd have an ExceptionHandlerAttribute which decided what to do about SecurityException's and UnauthorisedAccessException's etc. But that's just me, J I like Troy Goode's implementation of the ErrorHandlerFilter where you would have multiple ErrorHandler attributes which describe what happens on a per Exception basis. This makes sense because you would have your ErrorHandlerFilter (and default ErrorHandler attributes) on your Controller base class so that it occurs first. And on Actions where you wanted something special to happen, you'd just add another ErrorHandler attribute to your action. It'd be nice to see some ActionFilterAttributes come with the Framework at some point!
Mads - March 13, 2008 - Other ideas for ActionFilters: - GZip Compression - OutputCache - HttpPostOnly (for only accepting POST's. Useful for Actions that takes a posted form to update a product or something.)
Nick Berardi - March 13, 2008 - Hi Rob, With all due respect, and this is going to sound blunt, but why are you reinventing the wheel with these ActionFilterAttributes? The PrincipalPermissionAttribute already does a wonderful job of what you described above. And it is built in to the run time at a lot lower and I imagine a lot less resource intensive point. See my post about it: http://www.coderjournal.com/2008/03/securing-mvc-controller-actions/ It also has been in the framework since .NET 1.0 and implements the CodeAccessSecurityAttribute, which has done a wonderful job at protecting the framework from outside intruders for a long time. I am just curious as to the benefits of the solution you listed above. Personally I would love to see OuputCache attributes and GZipCompressionAttribute, since they are not currently built in to the .NET framework. Also Mads I have created an HttpPostOnlyAttribute if you are looking for one. Just contact me and I will send it to you. Nick
Tom - March 13, 2008 - Nick, Rob doesn't seem to be re-inventing the wheel. The PrincipalPermission attribute throws security exceptions (correct me if I am mistaken) whereas the ActionFilter attribute allows the attribute to be acted upon as a sequence of the controller execution. This allows for a graceful redirect (IE not logged in -> Go to login page). This is a key feature. In your example, a security exception would be thrown and then what would you do? Rob, On thing that would also be nice would be to have this attribute be applicable at the controller level, not just the action level. -Tom
Rob Conery - March 13, 2008 - @Nick - I actually worked up a post about 2 months ago using PrinciplePermission but I had to go in and catch the Exception in the Global.asa. Yes it works but this is much nicer since I have access to the context of the call and I can do things like test for roles, etc.
tgmdbm - March 13, 2008 - @Tom ActionFilter attributes ARE applicable at the controller level. You can even apply them to your controller base class (at any point in the inheritance chain), or the virtual method which your controller action overrides. @Rob, what do you think about throwing a SecurityException in one ActionFilterAttribute and catching in another? Too messy?
Nick Berardi - March 13, 2008 - @Rob: I have to admit your solution is much nicer on the eyes. But one recommendation I would make is to set the status to "401 Unauthorized" and let the appropriate authentication module handle the authentication. Because you attribute assumes everybody is using Forms authentication. Passport, Windows, Basic, and Custom authentication solutions would have some trouble using your attribute. Also a merger of both would also be nice, so if I only wanted users authenticated with "Admin" role. Because you can be authenticated and still have roles assigned to you. Also I ran in to the same issue with exceptions. Except I used the ActionFilterAttribute to catch the exception and do the redirect. This has the nice side effect catching any other exceptions and crafting the response too. But I acknowledge your simplicity point of view. Source for my ExceptionHandlerAttribute: http://code.google.com/p/coderjournal/source/browse/trunk/ManagedFusion.Web.Mvc/ExceptionHandlerAttribute.cs So I would do the following to my method to let the 401 float down to the EndResponse and then let the FormsAuthenticationModule or WindowsAuthenticationModule pick up the HTTP 401 Status Code and do what ever it does depending on what type of authentication you are using. [PrincipalPermission(SecurityAction.Demand, Name = "SiteAdmin")] [ExceptionHandler(401, "Unauthorized", typeof(SecurityException)] public void RolesAdmin () { RenderView("RolesAdmin"); } Or I can redirect them to the correct action depending on the exception that is thrown [PrincipalPermission(SecurityAction.Demand, Name = "SiteAdmin")] [ExceptionHandler("Login", "User", typeof(SecurityException)] public void RolesAdmin () { RenderView("RolesAdmin"); } Thanks, Nick
Rob Conery - March 13, 2008 - @Nick - good point RE 401 - although my implementation is specific to FormsAuth but yes, you're correct in that someone might indeed want Windows or Passport(????). All in all i think if you can accomplish your goal with less code - that's the key. You're code will work, but it involves a bit more work and I'm not clear on the advantage yet. Having said that, as you can see from my FxCop Justification above I shouldn't be writing code :) so I don't want to speak out of turn.
Nick Berardi - March 13, 2008 - Don't get me wrong I actually like the simplicity and better naming of your attribute. I also acknowledge that the cost of the exception being thrown might out weight the code security as far as performance goes. I just threw Passport in there because I know the Windows Live team is working on the new Passport. Also I think you could even do it with less code by just setting doing the following public class RequiresAuthenticationAttribute : ActionFilterAttribute { public ActionFilterAttribute () { } public bool Authenticated { get; set; } public string Role { get; set; } public override void OnActionExecuting(FilterExecutingContext filterContext) { if (!Authenticated && String.IsNullOrEmpty(Role)) return; bool authorized = true; if (filterContext.HttpContext.User == null || filterContext.HttpContext.User.Identity == null) authorized = false; else if (Authenticated && !filterContext.HttpContext.User.Identity.IsAuthenticated) authroized = false; else if (!filterContext.HttpContext.User.IsInRole(Role)) authorized = false; if (!authorized) { filterContext.HttpContext.Response.StatusCode = 401; filterContext.HttpContext.Response.StatusDescription = "Unauthorized"; filterContext.HttpContext.Response.End(); filterContext.Cancel = true; } } } This would mimic the behavior of the .NET framework as it currently is. By just setting that status code whatever authentication module is being used picks it up and does it's own magic. Nick
SubC - March 17, 2008 - My unit tests attach a customer Principal/Identity to the current thread so I can test controller actions as AppUser, AppManager, etc. Using PrincipalPermission.Demand works because the same test occurs whether I am using the web (HttpContext.User) or unit test console (System.Threading.Thread.CurrentPrincipal). I started to try out the RequiresAuthenticationAttribute sample, but it looks like it'll choke my tests since HttpContext is always null so filterContext.HttpContext.User.Identity.IsAuthenticated will fail. What am I missing? Thanks.
Khai Wan - March 19, 2008 - Excellent ...
ironside - March 23, 2008 - @subc, sounds like perhaps you should mock HttpContext.User.Identity and the IsInRole().

This would have the additional benefit of not needing to run your test console as different users/roles in order to test them.

I know MVC is riddled with interfaces so mocking is easy ( a design flaw, imho; too many interfaces introduced soley to satisfy rhinomocks)
Emad Ibrahim - April 7, 2008 -

Very nice solution Rob, but when I put a breakpoint inside my OnActionExecuting, it doesn't break...

I would also prefer to redirect to an action than to a page which I guess I could do by redirecting to /controller/action

@subc and @ironside: I blogged about mocking the user identity at www.emadibrahim.com/.../unit-test-linq-

Thanks.

Emad

Ben - May 2, 2009 - Very cool, thanks! Looks like the smart solution to this if you ask me.
Nag - May 5, 2009 - While the Forms authentication is capable of redirecting the unauthenticated users to the login page if he calls the controller action with out login, at what case do we need to write an RequiresAuthenticationAttribute? What is the benifit of this while forms authentication already does it by default?
&raquo; How to say to the compilator : &#8220;Leave me alone&#8221; blog.dervalp.com - May 14, 2009 - [...] You can find more info : here I found that information in the Rob Conery’s blog. [...]
MVC: Membership Starter Kit &laquo; SquaredRoot - June 8, 2009 - [...] big thanks goes out to Rob Conery, as I borrowed his recent Authentication Filters and included those, along with my recently released Error Handling [...]
MVC: Action Filter for Handling Errors &laquo; SquaredRoot - June 8, 2009 - [...] however, was a new built-in filter framework. Rob Conery has already gone through the trouble of creating authentication filters that cover most of the functionality I had before, but I have yet to see an implementation of a [...]
Todd - September 24, 2009 - Great tutorial. I use forms authentication to secure most of my controllers, but I am creating a custom security attribute that validates the IP of the caller. How can I remove forms authentication from these methods so that it won't intercept the HTTP request before my filter can fire? Here's what I've added to my web.config to allow anonymous access to these methods.


















Gecko