Friday, July 8, 2011

MVC3 Security: A Fluent Way

In Asp MVC you can control user authorization by using security attributes to decorate the controller actions for authorization, for example, in the following AccountController class, the Authorize attribute decorates the ChangePassword action method so it will only allow logged in users to change their passwords. One way to test this is,


Code Snippet

[Test]
        public void Verify_ChangePassword_Method_Is_Decorated_With_Authorize_Attribute()
        {
            var controller = new AccountController();

            var type = controller.GetType();
            var methodInfo = type.GetMethod("ChangePassword", new Type[] { typeof(ChangePasswordModel) });

            var attributes = methodInfo.GetCustomAttributes(typeof(AuthorizeAttribute), true);
            Assert.IsTrue(attributes.Any(), "No AuthorizeAttribute found on ChangePassword(ChangePasswordModel model) method");
        }



While the above code certainly tests that the Change Password method has the Authorize Attribute the usage of Reflection is quite heavy. For me it makes the test a bit too verbose. Enter the Fluent Security framework. To quote the website “Fluent Security provides a fluent interface for configuring security in ASP.NET MVC. No attributes or nasty xml, just pure love.” Also available via NuGet:

[sourcecode language="csharp"]
PM> Install-Package FluentSecurity
[/sourcecode]

Fluent Security enables the implementation of configuration based security. Let’s start by removing the Authorize attribute from the Account Controller:


Code Snippet

[HttpPost]
        public ActionResult ChangePassword(ChangePasswordModel model)
        {
            if (ModelState.IsValid)
            {
                // ChangePassword will throw an exception rather
                // than return false in certain failure scenarios.
                bool changePasswordSucceeded;
                try
                {
                    MembershipUser currentUser = Membership.GetUser(User.Identity.Name, true /* userIsOnline */);
                    changePasswordSucceeded = currentUser.ChangePassword(model.OldPassword, model.NewPassword);
                }
                catch (Exception)
                {
                    changePasswordSucceeded = false;
                }

                if (changePasswordSucceeded)
                {
                    return RedirectToAction("ChangePasswordSuccess");
                }
                else
                {
                    ModelState.AddModelError("", "The current password is incorrect or the new password is invalid.");
                }
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }



Now to configure Fluent Security to secure the ChangePassword action, you can place the code the the Application start event of the Global.asax file. I prefer keep as much code out of that as possible and use a Application Bootstrapper.


Code Snippet

public class SecurityBootstrapper : IBootstrapThisApp
        {
            public void Execute()
            {
                SecurityConfigurator.Configure(configuration =>
                {
                    // Let Fluent Security know how to get the authentication status of the current user
                    configuration.GetAuthenticationStatusFrom(() => HttpContext.Current.User.Identity.IsAuthenticated);

                    // This is where you set up the policies you want Fluent Security to enforce
                    configuration.For<HomeController>().Ignore();

                    //configuration.For<AccountController>().DenyAuthenticatedAccess();
                    configuration.For<AccountController>(x => x.LogOff()).DenyAnonymousAccess();
                });

                GlobalFilters.Filters.Add(new HandleSecurityAttribute(), 0);
            }
        }

        public interface IBootstrapThisApp
        {
            void Execute();
        }



By default Fluent Security will throw an exception if a missing configuration is encountered for a controller action. If you don't want Fluent Security to handle security for all controllers you can tell it to ignore missing configurations. You can do this by adding configuration.IgnoreMissingConfiguration(); to your configuration expression. Testing becomes pretty simple using Fluent Securities Test Helper, available as a NuGet package.



Code Snippet

[Test]
        public void Should_Have_Correct_Security_Configuration()
        {
            new SecurityBootstrapper().Execute();
            var results = SecurityConfiguration.Current.Verify(x =>
            {
                x.Expect<HomeController>().Has<IgnorePolicy>();
                x.Expect<AccountController>().Has<DenyAuthenticatedAccessPolicy>();

                x.Expect<AccountController>(y => y.LogOff()).Has<DenyAnonymousAccessPolicy>().DoesNotHave<DenyAuthenticatedAccessPolicy>();
            });

            Assert.That(results.Valid(), results.ErrorMessages());
        }




I've only just started to implement security using the above methods and so far I am liking the framework. Testing becomes easier and it's a lot easier to get an overview of your Web Applications Security configuration