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

410 Views Asked by At

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.

1

There are 1 best solutions below

2
MrC aka Shaun Curtis On BEST ANSWER

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