Using AuthorizeAttribute for modules in a multi-tenancy web platform

Imagine that you are developing a web platform (maybe even a SaaS product) to support multiple customers. It could be built as a multi-tenancy application, that offers a basic subscription and a variety of optional add-ons. How could authorization of access to each add-on be easily implemented?

Well, I did an ASP.Net MVC project where a number of new features were to be functionality sold as modules. These modules should be available as individual add-ons to a basic subscription. Some of the features would be a separate page, some of them would be overlay windows. However, they were all developed in ASP.Net MVC in separate controllers. One controller per module.

Of course, the features should only be available or visible to customers who actually had purchased the specific add-ons. It was a central requirement.

As a developer I am generally a fan of generic solutions. The kind of solutions that can be reused without duplication. To make that, I created a custom C# attribute, extending the built-in AuthorizeAttribute, and applied it to all the module controller classes.

The solution was like this:

  1. For a given customer, store the key of each available modules in a database table.
  2. Look up the available modules for a customer when one of their users log in (and cache it).
  3. On each of the controllers, add a custom authorization attribute.

On the attribute class I added a property to set the a key of the module. That key should be stored in the database. If the list of available modules contain that key, then ASP.Net MVC should authorize access to the module, unless another authorization module rejected the user.

Here is a code example.

public class AuthorizeModuleAttribute : AuthorizeAttribute
{
    private readonly ModuleService _moduleService;

    public string ModuleKey { get; set; }

    public AuthorizeModuleAttribute()
    {
        _moduleService = new ModuleService();
    }

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        if (httpContext == null)
        {
            throw new ArgumentNullException(nameof(httpContext));
        }

        if (string.IsNullOrWhiteSpace(ModuleKey))
        {
            return false;
        }

        string customerId = UserContext.CustomerId;
        return customerId != null && _moduleService.HasEnabledModule(customerId, ModuleKey);
    }
}

Notice that the code is calling a ModuleService? It just looks at a cached list of module keys for this specific customer ID, then searches for the specific module key. If it is in the list, it returns true, and so does the check in the authorization attribute.