Securing an ASP.NET MVC Application Using FluentSecurity
The excellent class library FluentSecurity allows you to easily secure an ASP.NET MVC application. By using it, you can benefit from the following main advantages:
- Authorization rules can be specified for both single action methods or entire controllers.
- Security is specified in a centralized place in a readable and maintainable way, thus making it unnecessary to decorate your controllers with
[Authorize]
attributes cluttering your code. - FluentSecurity forces you by default to explicitly specify authorization rules for all action methods to prevent you from forgetting to secure them.
- You can extend the library and implement your own authorization rules and violation handlers.
- The specified security configuration is unit testable; thus, you can verify that it is working correctly.
#Integration into an MVC Application
The quickest way to integrate FluentSecurity into an ASP.NET MVC application is installing the NuGet package FluentSecurity, so open up the NuGet Package Manager Console, make sure your MVC project is selected in the Default project dropdown list, and run the following command:
Install-Package FluentSecurity
Your project now references the assembly FluentSecurity — besides that, nothing has been changed.
#Retrieving the User's Authentication Status
To let FluentSecurity handle authorization globally within your application, open the Global.asax file and add the HandleSecurityAttribute
to the global filter collection within the RegisterGlobalFilters
method (make sure that you have imported the namespace FluentSecurity):
filters.Add(new HandleSecurityAttribute(), 0);
It is important to set the attribute's filter run order to 0 so that FluentSecurity can enforce security rules before anything else in the request pipeline is executed.
Furthermore, add the following security configuration shown below to the Application_Start method before the RegisterGlobalFilters
method is called — otherwise, FluentSecurity will throw an exception stating that no security rules have been specified:
SecurityConfigurator.Configure(configuration =>
{
// Tell FluentSecurity where to obtain the user authentication status from
configuration.GetAuthenticationStatusFrom(() =>
HttpContext.Current.User.Identity.IsAuthenticated);
});
The above example instructs FluentSecurity to call the specified Func<bool>
delegate – which is querying the HttpContext.User.Identity.IsAuthenticated
property used by ASP.NET Forms Authentication – to retrieve the current user's authentication status.
Note that, when you run the application, you will receive a ConfigurationErrorsException
— this is by design! By default, FluentSecurity throws that exception whenever an action method for which there is no security explicitly specified is called. If you don't like this feature, you can easily turn it off:
configuration.IgnoreMissingConfiguration();
However, I strongly recommend not to ignore missing configurations, for the thrown exception prevents you from forgetting to secure action methods (or controllers) by accident.
#Specifying Security Policies
So far, we have configured authentication information, but we haven't specified any authorization rules yet. FluentSecurity uses the concept of Policies to configure authorization rules for either entire controllers or single action methods.
To secure your HomeController
from unauthenticated access, add the following line to the configuration:
configuration.For<HomeController>().DenyAnonymousAccess();
The DenyAnonymousAccess
extension method registers the DenyAnonymousAccessPolicy
for the entire HomeController
. If an unauthenticated user attempts to call any action methods living inside the controller, a PolicyViolationException
is thrown. An authenticated user, on the other hand, will pass.
You can also add the same policy to all controllers in your application:
// Secure all action methods of all controllers
configuration.ForAllControllers().DenyAnonymousAccess();
// Make sure that users can still log on
configuration.For<AccountController>(ac => ac.LogOn()).Ignore();
The lambda expression ac => ac.LogOn()
restricts the IgnorePolicy
to the LogOn
action method. At that point of time, only parameterless methods can be selected, but a future release of FluentSecurity is likely to include support for parametrized methods.
In the current version 1.4.0, the following policies are available out of the box:
DelegatePolicy
— The specified delegate must return true or a success result.DenyAnonymousAccessPolicy
— The user must be authenticated.DenyAuthenticatedAccessPolicy
— The user must be anonymous.IgnorePolicy
— All users are allowed.RequireAllRolesPolicy
— The user must be authenticated with all of the specified roles.RequireRolePolicy
— The user must be authenticated with at least one of the specified roles.
#Implementing a Custom Policy
If none of the existing policies fulfills your needs, you can create your own policy by implementing the ISecurityPolicy
interface and with it its Enforce
method. The following example shows the implementation of a custom policy that restricts access to a controller to requests on weekends:
public class WeekendsOnlyPolicy : ISecurityPolicy
{
public PolicyResult Enforce(ISecurityContext context)
{
DateTime now = DateTime.Now;
bool isWeekend = now.DayOfWeek == DayOfWeek.Saturday
|| now.DayOfWeek == DayOfWeek.Sunday;
return isWeekend
? PolicyResult.CreateSuccessResult(this)
: PolicyResult.CreateFailureResult(this, "Access denied!");
}
}
#Handling Policy Violations
When a policy is violated, FluentSecurity will throw a PolicyViolationException
. You can, of course, catch the exception regularly and do with it whatever you like. However, the cleaner approach would be to register a Policy violation handler which has to meet certain criteria:
- It has to implement the
IPolicyViolationHandler
interface (a singleHandle method
accepting aPolicyViolationException
and returning anActionResult
). - The handler name has to match the format
<PolicyName>ViolationHandler
, since FluentSecurity uses a naming convention to locate the correct policy violation handler.
The recommended way to register custom policy violation handlers is by using an IoC container. Please refer to the documentation page for more information on how to create and register policy violation handlers using a dependency injection framework.
#Testing Your Security Configuration
To ensure that your security rules are configured correctly, you can test them in a very readable, fluent way using the NuGet package FluentSecurity.TestHelper
:
Install-Package FluentSecurity.TestHelper
Given that you have encapsulated the security configuration in the ConfigureFluentSecurity
method of a separate Bootstrapper
class, possible expectations for the security configuration created before could look like the following:
// Arrange
Bootstrapper.ConfigureFluentSecurity();
// Act
var results = SecurityConfiguration.Current.Verify(expectations =>
{
expectations.Expect<HomeController>().Has<DenyAnonymousAccessPolicy>();
expectations.Expect<AccountController>().Has<DenyAnonymousAccessPolicy>();
expectations.Expect<AccountController>(ac => ac.LogOn()).Has<IgnorePolicy>();
});
// Assert
bool isValidConfiguration = results.Valid();
Assert.IsTrue(isValidConfiguration);
Besides the Has
extension method, there is also a DoesNotHave
version expecting that a certain policy is not assigned to an action method or a controller. For more information on how to test your security configuration, please have a look at the corresponding documentation page.
#Further Resources
If you are interested in reading more about the project or its author, here are some interesting references:
- GitHub Repository: kristofferahl/FluentSecurity
- NuGet Gallery: Packages » FluentSecurity
- Author: Kristoffer Ahl (@kristofferahl)