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.
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;
}
}
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