Applying Authorisation Rules to a Folder of Razor Components/Blazor Pages

There’s a lot of flexibility in how you can use authorisation rules in Razor components*, but a frustration of this approach is that it seems like you have to slap @attribute [Authorize(Policy = "PolicyName")] at the top or every page with no clear way of applying a given policy to a whole folder of .razor files. After much research and testing, I’ve found how to go about it!

Before I go any further though, I’d like to give a big shout-out to the answer on Stack Overflow that pointed me in the right direction. Although this answers the basic question of applying one rule to multiple Razor components, I decided to expand on it and write a full blog post mainly because the answer was initially quite hard to find and I had more questions about how multiple rules were resolved against each other!

*Throughout this blog post I’m going to be referring to “Razor components” which can largely be taken to mean .razor/.razor.cs files. Confusingly Microsoft decided to also use the “Razor” term to refer to the general syntax that allows for .NET code in webpages such as .cshtml files, which explicitly don’t work with this approach (although thankfully the documentation for applying authorisation rules to .cshtml files is a bit more robust). TL;DR: Razor components == .razor/.razor.cs files.

Approach

I’ve put together a Blazor app for testing/demonstrating the different rules and how the edge cases all work in practice, which is where the header image and following examples are from! Feel free to use it as a reference or to grab a copy to test your own edge cases.

Let’s imagine we’ve got the following structure of files in our “Pages” directory:

  • Anonymous
    • Index.razor
  • FailurePolicy
    • Index.razor
  • SuccessPolicy
    • Index.razor

Let’s also assume that we’ve already got a couple of policies set up: FailurePolicy will always fail (to simulate attempting to access a page that we don’t have permissions to) while SuccessPolicy will always succeed (simulating a policy that we satisfy).

In this case we want anything placed in the “Anonymous” directory to use the AllowAnonymous attribute while the “FailurePolicy” and “SuccessPolicy” directories will use the Authorize attribute with the “FailurePolicy” and “SuccessPolicy” policies respectively.

If we wanted to do this manually, we could add the required attribute by placing an attribute directive in each file, such as the following for FailurePolicy/Index.razor:

@page "/FailurePolicy"
@attribute [Authorize(Policy = "FailurePolicy")]

<h1>Failure Policy Index</h1>

While this works, it’s a very manual process that is prone to potential errors (imagine not adding/updating a policy for an admin page?). How do we go about applying an attribute to a whole folder?

That’s where _Imports.razor comes in! _Imports.razor is a special file that is used to include imports for a whole folder of Razor components/Blazor pages so that they don’t need to be included manually in each page. The magic of this approach is that other directives (such as the attribute directive) can be applied at a top-level and apply to all of the pages in that directory and all sub-directories! Let’s look at this in practice.

You can see below where the _Imports.razor files have been added to our previous example:

  • Anonymous
    • _Imports.razor
    • Index.razor
  • FailurePolicy
    • _Imports.razor
    • Index.razor
  • SuccessPolicy
    • _Imports.razor
    • Index.razor

All you need in _Imports.razor is the authorisation rule, such as the following for the SuccessPolicy/_Imports.razor:

@attribute [Authorize(Policy = "SuccessPolicy")]

Assuming you have no additional requirements (and if you do, check the edge cases section to see how rules are resolved), this is all you need to do to get the pages in a given directory to automatically inherit authorisation rules!

Edge Cases

As mentioned before, I’ve set up a solution for testing authorisation rules with implementations of the examples below!

As with anything in software, there are a couple of caveats/edge cases that you might want to consider if you’re nesting instances of _Imports.razor in subdirectories or declaring additional authorisation rules directly on pages!

One of the important things to consider is that a given rule applies to the folder hierarchy in your solution and is not influenced by @page directive. For example, if your SuccessPolicy/Index.razor page had a @page directive of “/FailurePolicy/Success” (instead of having a path that included “SuccessPolicy”) the authorisation rules will still apply the same as for any other page in that directory in your solution.

It’s also worth noting that this approach won’t affect .cshtml files; you’ll have to use the options.Conventions.AuthorizeFolder approach for that situation!

General Rules

It’s worth testing your specific implementation to see if your policies are applying as expected, but from playing around with different setups I found the following:

  1. An Authorize attribute on a specific page will always apply to that page.
  2. If there is an _Imports.razor in one of the folders that contains the page (up to the root folder), any authorisation rules will also apply (i.e. rules declared directly on the page and in the _Imports.razor file will need to be satisfied). The closest instance of _Imports.razor will be used, regardless of if there is another instance further up the directory structure.

Specific Edge Cases

Let’s put together a more complex file structure. I’ve added some more pages and explicitly marked where authorisation rules are in the layout:

  • Anonymous
    • _Imports.razor (AllowAnonymous)
    • Index.razor
  • FailurePolicy
    • ChangedSubdirectory
      • _Imports.razor (SuccessPolicy)
      • ChangedSubdirectoryPage.razor
    • UnchangedDirectory
      • UnchangedSubdirectoryPage.razor
    • _Imports.razor (FailurePolicy)
    • Index.razor
    • PolicyChangedExplicitly.razor (SuccessPolicy)
  • SuccessPolicy
    • _Imports.razor (SuccessPolicy)
    • Index.razor
    • PageDifferentToLocation

And here is a table explaining which policies are applied and why! Again, if it’s not clear from reading I’d recommend for you to grab a copy of my authorisation rule test solution to try these exact examples out interactively!

DirectoryPageApplied PolicyPolicy SourceWhy It’s Interesting
AnonymousIndexNone (allows for anonymous access)Inherited from _Imports.razor in the root of Anonymous
Failure PolicyChanged Subdirectory PageSuccess Policy_Imports.razor in ChangedSubdirectoryThe version of _Imports.razor in ChangedSubdirectory overrides the rule applied in the top-level FailurePolicy _Imports.razor
Failure PolicyUnchanged Subdirectory PageFailure PolicyInherited from _Imports.razor in the root of FailurePolicySince there are no changes to the declared authorisation rules, this page inherits from the top-level _Imports.razor
Failure PolicyIndexFailure PolicyInherited from _Imports.razor in the root of FailurePolicy
Failure PolicyPolicy Changed ExplicitlySuccess Policy and Failure PolicyExplicitly specified in file (SuccessPolicy) and _Imports.razor in the root of FailurePolicy (FailurePolicy)Shows the nature of explicit rules in a page combining with inherited rules from _Imports.razor
Success PolicyIndexSuccess PolicyInherited from _Imports.razor in the root of SuccessPolicy
Success PolicyPage Different To LocationSuccess PolicyInherited from _Imports.razor in the root of SuccessPolicyThe URL (/FailurePolicy/PageDifferentToLocation) implies that it would inherit from FailurePolicy, but because the actual file is located in the SuccessPolicy directory it inherits from that version of _Imports.razor

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.