When building a big web application with ASP.NET MVC 3, I ran into a problem to secure my web application in a maintainable way. There are lots of examples with attributes, but this isn’t maintainable. So I started searching for other solutions, however most of the information is leaning on those un-maintainable attributes I finally found “Fluent Security“.
What does Fluent Security offer you?
Fluent Security provides a fluent interface for configuring security in ASP.NET MVC. No attributes or nasty XML, just pure love. Go get it on NuGet!
What does that mean?
Well, it simply means you can bootstrap your security just from your Application_Start()
and maintain it in a single place. Besides that, you can easily unit test your security setup. So you can test if your security configuration matches the security setup you described in your unit tests. This is a huge advantage because you know for sure the controller actions are secured the way you like it without having the need to click through your complete web application. You can only fail when not defining your tests correctly. That’s not all… You can easily extend, modify, etc. by adding your own implementations of the interfaces.
Let me start to show you some code so you see for yourself how easy it is! My example is based on a default MVC 3 application and I have added a CategoryController
like below to have some extra actions for my example.
public class CategoryController
{
[HttpGet]
public ActionResult AddNewCategory()
{
return View(new CategoryModel());
}
[HttpPost]
public ActionResult AddNewCategory(CategoryModel model)
{
if (!ModelState.IsValid) return View(model);
return RedirectToAction("AddNewCategory");
}
}
First of all, I create a static
class for my bootstrap code! I leave the implementation blank because I will first implement some tests.
public static class SecurityBootstrapper
{
public static void BootUp()
{
}
}
Then, you start to write some tests for your security setup. I would advice you to be very explicit in your test setup although you won’t have to. This way, you are 100% sure you secured it exactly the way you want and it is 100% transparent. So never write a test for your whole controller, but do it for every specific action.
[TestFixture]
public class FluenSecuritySetupTests
{
[SetUp]
public void SetUp()
{
BootStrapper.ConfigureFluentSecurity();
}
[Test]
public void anonymous_access_should_be_allowed_for_the_logon_and_home_index_actions()
{
var results = SecurityConfiguration.Current.Verify(expectations =>
{
expectations.Expect<UserController>(c => c.LogOn(string.Empty)).Has<IgnorePolicy>();
expectations.Expect<HomeController>(c => c.Index()).Has<IgnorePolicy>();
});
Assert.That(results.Valid(), results.ErrorMessages());
}
[Test]
public void adding_a_new_catogegory_requires_a_system_administrator_role()
{
var results = SecurityConfiguration.Current.Verify(expectations =>
{
expectations.Expect<CategoryController>(c => c.AddNewCategory()).Has(new RequireRolePolicy(AppRoles.SystemAdministrator));
expectations.Expect<CategoryController>(c => c.AddNewCategory(null)).Has(new RequireRolePolicy(AppRoles.SystemAdministrator));
});
Assert.That(results.Valid(), results.ErrorMessages());
}
}
After we have defined our tests for our security setup, we can implement the actual setup.
public static class SecurityBootstrapper
{
public static void BootUp()
{
SecurityConfigurator.Configure(configuration =>
{
configuration.ResolveServicesUsing(type =>
BootStrapper.Container.ResolveAll(type).Cast<object>());
configuration.GetAuthenticationStatusFrom(() =>
HttpContext.Current.User.Identity.IsAuthenticated);
configuration.GetRolesFrom(Roles.GetRolesForUser);
configuration.ForAllControllersInAssembly
(typeof(HomeController).Assembly).DenyAnonymousAccess();
configuration.For<HomeController>
(c => c.Index()).Ignore();
configuration.For<UserController>
(c => c.LogOn()).Ignore();
configuration.For<UserController>
(c => c.ResetPassword()).RequireRole(AppRoles.UserAdministrator);
configuration.For<CategoryController>
(c => c.AddNewCategory()).RequireRole(AppRoles.SystemAdministrator);
configuration.For<CategoryController>
(c => c.AddNewCategory(null)).RequireRole(AppRoles.SystemAdministrator);
}
}
}
As you probably have seen already, we configure the security for both the get and post actions. Now we can run our tests to see if we implemented the security like we have defined them in our tests. When all your tests succeeded, we are ready to enable it in our web application and define handlers for our policy violations.
public class MvcApplication : HttpApplication
{
public static IWindsorContainer Container { get; private set; }
protected void Application_Start()
{
SecurityBootstrapper.BootUp();
Container = new WindsorContainer().Install(FromAssembly.This());
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
}
First of all, we called our SecurityBootstrapper.BootUp()
method in our global.asax to configure our security. Then I used Castle Windsor as dependency container to register my dependencies. Of course, you can use your own favorite IoC container. Or plumb your own implementation. In order to install my Fluent Security dependencies, I implemented a Windsor installer which takes care of registering the dependencies in the container.
public class WindsorInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(AllTypes.FromThisAssembly().BasedOn(
typeof(IPolicyViolationHandler)).Configure(h => h.LifeStyle.Singleton));
}
}
As you can see, I install all PolicyViolationHandlers
in my assembly. As an example, I have added some implementations for a security policy violation handler.
public class DenyAnonymousAccessPolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
return new HttpUnauthorizedResult(exception.Message);
}
}
public class RequireRolePolicyViolationHandler : IPolicyViolationHandler
{
public ActionResult Handle(PolicyViolationException exception)
{
var rvd = new RouteValueDictionary(new
{
area = "",
controller = "Error",
action = "HttpForbidden",
statusDescription = exception.Message
});
return new RedirectToRouteResult(rvd);
}
}
These violation handlers are mapped by naming convention inside Fluent Security. So a RequireRolePolicy
needs a RequireRolePolicyViolationHandler
, etc.
I think I gave you a good impression of the power of Fluent Security. There are lots of ways to make your own components for Fluent Security when the defaults don’t work for you. It is open source available on Github, so you can easily participate and improve Fluent Security. The documentation is pretty good and Kristoffer Ahl really helped me out with some small issues I had when trying it out first time. Please share the article if you liked it and you really should try it out. Have fun!