blazorblazor-server-sideasp.net-blazor

Securing a folder or page, to a role, in Blazor Server


I have a blazor server app, and a number of admin pages.

I have my pages in an admin folder, and in that folder I have an _imports.razor file that authorizes a specific AD group:

@attribute [Authorize(Roles = "MyDomain\\MyAppAdministrators")]

That prevents a user from trying to navigate to admin pages.

Anywhere I have links on other pages to admin pages I wrap them in AuthorizeView components:

<AuthorizeView Roles="MyDomain\MyAppAdministrators">
    <li class="nav-item">
        <NavLink class="nav-link" href="Admin/LaunchCodes">Enter Launch Codes</NavLink>
    </li>        
</AuthorizeView>

This hides the link if you're not an administrator.

My question is, I want the ad group to be configurable. This is easy to do for the AuthorizeView, but the Authorize attribute is compile time. Is there some way I can set up authorization for pages that I can configure at a folder leve? I'm hoping I don't have to write code on each page because that is vulnerable to developer forgetfulness, or breakage over time as.

I've tried...

builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/Admin");
});

But that has no affect, and my understanding is that Blazor isn't compatabile with that approach anyhow.


Solution

  • My question is, I want the ad group to be configurable

    You do so by moving to Policy Based Authorization. Here's some example code. I've included a custom IAuthorizationRequirement to show how it's defined and setup.

    Some constants to define our names

    public static class AuthRoles
    {
        public const string AdminRole = "MyDomain\\MyAppAdministrators";
        public const string UserRole = "UserRole";
        public const string VisitorRole = "VisitorRole";
    }
    
    public static class AuthPolicyNames
    {
        public const string UserPolicy = "UserPolicy";
        public const string VisitorPolicy = "VisitorPolicy";
        public const string AdminPolicy = "AdminPolicy";
        public const string CustomPolicy = "CustomPolicy";
    }
    

    Define the Application Policies and create a dictionary matching polices (defined as strings) and AuthorizationPolicy objects.

    public static class AppPolicies
    {
        public static AuthorizationPolicy AdminAuthPolicy
            => new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .RequireRole(AuthRoles.AdminRole)
            .Build();
    
        public static AuthorizationPolicy VisitorAuthPolicy
            => new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .RequireRole(AuthRoles.AdminRole, AuthRoles.UserRole, AuthRoles.VisitorRole)
            .Build();
    
        public static AuthorizationPolicy UserAuthPolicy
            => new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .RequireRole(AuthRoles.AdminRole, AuthRoles.UserRole)
            .Build();
    
        public static AuthorizationPolicy CustomAuthPolicy
            => new AuthorizationPolicyBuilder()
            .RequireAuthenticatedUser()
            .AddRequirements(new CustomAuthorizationRequirement())
            .Build();
    
        public static Dictionary<string, AuthorizationPolicy> Policies = new Dictionary<string, AuthorizationPolicy>()
        {
            {AuthPolicyNames.AdminPolicy, AdminAuthPolicy},
            {AuthPolicyNames.UserPolicy, UserAuthPolicy},
            {AuthPolicyNames.VisitorPolicy, VisitorAuthPolicy},
            {AuthPolicyNames.CustomPolicy, CustomAuthPolicy},
        }
    
        public static void AddAppPolicyServices(this IServiceCollection services)
        {
            services.AddScoped<IAuthorizationHandler, CustomAuthorizationHandler>();
        }
    }
    

    And in Program:

        services.AddAppPolicyServices();
        // Adds the runtine policies
        services.AddAuthorization(config =>
        {
            foreach (var policy in AppPolicies.Policies)
            {
                config.AddPolicy(policy.Key, policy.Value);
            }
        });
    

    and use:

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

    The Custom Handler set of classes defined in the policies (to demo how to do one):

    public class CustomAuthorizationRequirement : IAuthorizationRequirement { }
    
    public class CustomAuthorizationHandler : AuthorizationHandler<CustomAuthorizationRequirement>
    {
        // Demo to show you cn inject any service
        private readonly NavigationManager _navigationManager;
    
        public CustomAuthorizationHandler(NavigationManager navigationManager)
            => _navigationManager = navigationManager;
    
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement)
        {
            // You can do this directly in the policy.  This is just a simple demo
            if (context.User.IsInRole("AdminRole"))
                context.Succeed(requirement);
    
            return Task.CompletedTask;
        }
    }
    

    Checking Authentication

    You can check authentication in any component or DI service:

    The basics are:

    Inject the AuthorizationService. This is how to do it in a component. In a service add it to the CTor.

    [Inject] private IAuthorizationService AuthorizationService { get; set; } = default!;
    

    And then you can authorize like this:

    var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy!);
    

    result.Succeeded tells you if you were authorized or not.

    Resource is just a generic object that you can pass into your custom AuthorizationHandler and cast back.

    This link will take you to the relevant code in AuthorizeViewCore - https://github.com/dotnet/aspnetcore/blob/f543e3552514c5c420eeddd55c505bbc131f10a6/src/Components/Authorization/src/AuthorizeViewCore.cs#L99