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:
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:
For those of you who aren't familiar with Attributes, you can also declare properties that can be set when the Attribute is used:
And you can set these properties like this:
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:
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.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
WebFormRouteHandlerto 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.
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:
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:
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: aspnetmvc
