Razor Pages let us organize code around individual pages, which can make development faster and more focused. In this article, we will configure authorization conventions that apply one or more policies across an entire site.

Authorization convention: what's the point?

Adding the [Authorize] attribute to every page is both tedious and risky because it is easy to forget one.

The less repetitive security code you write, the lower the risk of introducing a bug. A central convention gives the site one place to manage authorization.

And how does that work?

The first step is to create one or more policies that define the required access level.

Creating policies

This example creates two claim-based policies, but the same approach works with policies based on requirements or other authorization rules.

Create a constants class containing the policy names:

public sealed class Policies
{
        public const string IsSuperAdmin = "IsSuperAdmin";
        public const string IsAdmin = "IsAdmin ";
}

Add the policies through AddAuthorization in ConfigureServices:

services.AddAuthorization(auth =>
{
   // vérification que le claim SuperAdmin existe et que sa valeur = True
   auth.AddPolicy(Policies.IsSuperAdmin, policy => policy.RequireClaim("SuperAdmin", "True"));

   // vérification que le claim Tenant existe
   auth.AddPolicy(Policies.IsTenant, policy => policy.RequireClaim("Tenant"));
});

Added conventions: Also in the ConfigureServices method we will change the default behavior of the AddMvc method by calling the AddRazorPagesOptions method.

   services.AddMvc()
        .AddRazorPagesOptions(options =>
        {
            // Toutes les pages dans le dossier SuperAdmin "devront" satisfaire la Policy IsSuperAdmin
            options.Conventions.AuthorizeFolder("/SuperAdmin", Policies.IsSuperAdmin);
            // Toutes les pages dans le dossier Tenant "devront" satisfaire la Policy IsTenant
            options.Conventions.AuthorizeFolder("/Tenant", Policies.IsTenant);

            // Toutes les pages dans le dossier Secured "devront" avoir un utilisateur connecté sans Policy particulière
            options.Conventions.AuthorizeFolder("/Secured");

            // Toutes les pages du dossier PublicPages ne requerront pas d'authentification
           options.Conventions.AllowAnonymousToFolder("/PublicPages");
        })
      .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Combining conventions

You can disable security on a single page as follows:

.AuthorizeFolder("/Admin", Policies.IsAdmin).AllowAnonymousToPage("/Admin/Public")

However, the reverse will not work:

.AllowAnonymousToFolder("/Public").AuthorizePage("/Public/Tenant",Policies.IsTenant)

Similarly, this agreement will not work because it will always verify that the user is a Tenant before verifying that the user is a SuperAdmin.

.AuthorizeFolder("/Tenant",Policies.IsTenant).AuthorizeFolder("/Admin/SA",, Policies.IsSuperAdmin)

However, this kind of behaviour may be desirable, to circumvent this limitation we will write our own convention and not use the “AuthorizeFolder” and AllowAnonymous : methods:

options.Conventions.AddFolderApplicationModelConvention("/", model =>
{
    string policy = null;
    // Si la route commence par "Tenant/SA" on choisit d'utiliser la Policy.IsSuperAdmin
    if (model.RouteTemplate.StartsWith("Tenant/SA"))
    {
        policy = Policies.IsSuperAdmin;
    }
    else if (model.RouteTemplate.StartsWith("Tenant"))
    {
       policy = Policies.IsTenant;
    }
    if (policy != null)
    {
         model.Filters.Add(new AuthorizeFilter(policy));
    }
    else
    {
    }
});

In a few lines of code we were able to apply all the security rules to our entire website. Happy coding 🙂