Reputation: 11
We have this custom Authorization scheme which I'm trying to solve with the ability to unit test and use dependency injection in .NET core. Let me explain the setup:
I created an interface IStsHttpClient and class StsHttpClient. This class connects to a internal web service that creates & decodes tokens. This has exactly 1 method "DecodeToken(string token)" and the constructor is very simple - it takes in an option object that is loaded from DI.
Then my AuthorizationHandler would in theory just use the IStsHttpClient to call and decode the token. My question is, based on the examples online I don't know how to properly specify/build the Authorization Handler (see code below).
Auth Code here:
public class MyAuthorizationRequirement : AuthorizationHandler<MyAuthorizationRequirement >, IAuthorizationRequirement
{
const string Bearer = "Bearer ";
readonly IStsHttpClient _client;
public BuzzStsAuthorizationRequirement([FromServices]IStsHttpClient client)
{
_client = client;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyStsAuthorizationRequirement requirement)
{
/* remaining code omitted - but this will call IStsHttpClient.Decode() */
My Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.Configure<StsHttpOptions>(Configuration.GetSection("StsConfigurationInfo"));
services.AddScoped<IStsHttpClient , StsHttpClient >();
services.AddAuthorization(options =>
{
options.AddPolicy("Authorize", policy =>
{
/* initialize this differently?? */
policy.AddRequirements(new MyStsAuthorizationRequirement( /* somethign is needed here?? */));
});
});
Upvotes: 1
Views: 3484
Reputation: 52
For other people looking to wrap authorization around an existing permission handler in C#9 NetCore5, the I found the following solution which allowed me to make use of the stock dependency injection container to inject a service into an AuthorizationHandler.
For me this required 5 new classes and some changes to Startup.cs
The following is my PermissionPolicyProvider.cs, this will represent a generic permission, and not a policy (I filter for permissions later)
using System.Data;
using System.Threading.Tasks;
using App.Models;
using App.Services.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
namespace App.Permissions
{
class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly AppUserManager<AppUser> _appUserManager;
public PermissionAuthorizationHandler(UserManager<AppUser> userManager)
{
_appUserManager = (AppUserManager<AppUser>)userManager;
}
#nullable enable
// public virtual async Task HandleAsync(AuthorizationHandlerContext context)
// {
// foreach (var req in context.Requirements.OfType<TRequirement>())
// {
// await HandleRequirementAsync(context, req);
// }
// }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var permissionsService = (PermissionService?) _appUserManager.Services.GetService(typeof(PermissionService))
?? throw new NoNullAllowedException("Null found when accessing PermissionService");
if (await permissionsService.Permitted(requirement.Permission))
{
context.Succeed(requirement);
}
}
#nullable disable
}
}
Next is my PermissionPolicyProvider.cs, this code allows us to filter out policies and to dynamically build a permission when received.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace App.Permissions
{
internal class PermissionPolicyProvider : IAuthorizationPolicyProvider
{
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public PermissionPolicyProvider(IOptions<AuthorizationOptions> options) =>
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
// Dynamically creates a policy with a requirement that contains the permission.
// The policy name must match the permission that is needed.
/// <summary>
///
/// </summary>
/// <param name="policyName"></param>
/// <returns></returns>
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (! policyName.StartsWith("Permission", StringComparison.OrdinalIgnoreCase))
{
// If it doesn't start with permission, then it's a policy.
// pass policies onward to default provider
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionRequirement(policyName));
return Task.FromResult(policy.Build());
}
}
}
Next up is the PermissionAuthorizationHandler.cs, this is where microsoft wants you to custom db checks, so if you don't want to separate your service layer you can stop after this. Note that you can handle one permission at a time or all at once (note the commented out code).
using System.Data;
using System.Threading.Tasks;
using App.Models;
using App.Services.Permissions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
namespace App.Permissions
{
class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly AppUserManager<AppUser> _appUserManager;
public PermissionAuthorizationHandler(UserManager<AppUser> userManager)
{
_appUserManager = (AppUserManager<AppUser>)userManager;
}
#nullable enable
// public virtual async Task HandleAsync(AuthorizationHandlerContext context)
// {
// foreach (var req in context.Requirements.OfType<TRequirement>())
// {
// await HandleRequirementAsync(context, req);
// }
// }
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var permissionsService = (PermissionService?) _appUserManager.Services.GetService(typeof(PermissionService))
?? throw new NoNullAllowedException("Null found when accessing PermissionService");
if (await permissionsService.Permitted(requirement.Permission))
{
context.Succeed(requirement);
}
}
#nullable disable
}
}
If you don't want the service layer separation, this is the last step for you. You just need to properly register all the services. Add the following to your Startup.cs
services.AddDbContext<PokeflexContext>
(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<PokeflexContext>()
.AddUserManager<UserManager<IdentityUser>>()
.AddDefaultTokenProviders();
To separate out the service layer, we need to extend the UserManager. UserManager actually gets access to the entire service layer injected into your app, but it hides it under a private modifier. Our solution is simple: extend the UserManager and override the constructor to pass on our service to a public variable instead. Here is my custom version as AppUserManager
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace App.Permissions
{
public class AppUserManager<TUser> : UserManager<TUser> where TUser : class
{
public IServiceProvider Services;
public AppUserManager(IUserStore<TUser> store,
IOptions<IdentityOptions> optionsAccessor, IPasswordHasher<TUser> passwordHasher,
IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators,
ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors,
IServiceProvider services, ILogger<UserManager<TUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators,
passwordValidators, keyNormalizer, errors, services, logger)
{
Services = services;
}
}
}
Last step here, we need to update Startup.cs again to reference our custom type. We also add another line here to ensure that if someone requests our service within an endpoint and not as an attribute they will get our custom AppUserManager. My final resulting ConfigureServices contents is as follows
services.AddDbContext<PokeflexContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddTransient<PermissionService>();
services.AddSingleton<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();
services.AddIdentity<AppUser, IdentityRole>()
.AddEntityFrameworkStores<PokeflexContext>()
.AddUserManager<AppUserManager<AppUser>>()
.AddDefaultTokenProviders();
services.AddScoped(s => s.GetService<AppUserManager<AppUser>>());
If you are already comfortable with service configuration then you probably don't need the following, but here is a simple service I created that the authorization handler can access via DI.
using System.Threading.Tasks;
using App.Data;
using Microsoft.EntityFrameworkCore;
namespace App.Services.Permissions
{
public class PermissionService
{
private PokeflexContext _dbContext;
public PermissionService(PokeflexContext dbContext)
{
_dbContext = dbContext;
}
public virtual async Task<bool> Permitted(string permission)
{
return await _dbContext.AppUsers.AnyAsync();
}
}
}
For some more information on permissioning, visit: https://github.com/iammukeshm/PermissionManagement.MVC
Upvotes: 1
Reputation: 1658
Nicholas,
You have to separate your handler and requirements here. In addition to that keep your DI stuff in the handler. Requirement itself is going to be either a DTO or an empty class with marker interface IAuthorizationRequirement.
Requirement:
public class MyAuthorizationRequirement : IAuthorizationRequirement
{
}
Handler:
public class MyAuthorizationHandler : AuthorizationHandler<MyAuthorizationRequirement>
{
const string Bearer = "Bearer ";
readonly IStsHttpClient _client;
public BuzzStsAuthorizationRequirement([FromServices]IStsHttpClient client)
{
_client = client;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, MyAuthorizationRequirement requirement)
{
...
}
}
Configuration:
services.Configure<StsHttpOptions>(Configuration.GetSection("StsConfigurationInfo"));
services.AddScoped<IStsHttpClient , StsHttpClient >();
services.AddAuthorization(options =>
{
options.AddPolicy("Authorize", policy =>
{
policy.AddRequirements(new MyAuthorizationRequirement());
});
});
Upvotes: 4