Search code examples
c#asp.net-coreasp.net-authorization

How to populate JWT claims dynamically so policies can be used in controllers?


In a new API app, going to use JWT bearer tokens for authorization. Idea of storing roles(claims) inside a token great, but there is some issues with using it like this:

Is setting Roles in JWT a best practice?

In our case, main problem would be payload size (100s of roles)

So, we decided to load roles on every request (DB call). However, I am not sure where in execution pipeline this should happen? We would like to use Policies and other built in ASP.NET Core functionality. Somewhere in pipeline we need to place a code to load claims from DB for a user, but before controller executed and policies checked.


Solution

  • I have done something similar, I check the user has the required permission on each request. Mine is done based on Features, but you can change it a bit to be based on Roles.

    I have a User with 1 Role, and then Role has N Features.

    For this I use created my own AuthorizationHandler as follows. AuthorizationHandlers are at the start of the pipeline, so it will go through all AuthorizationHandlers before hitting the controller endpoint. Offical Docs here

    'IFeaturesProvider': is just my Business layer to retrieve features from DB.

    ICustomUserContext: Is a wrapper around HttpContextAccessor.HttpContext.User (I will post it at the end of answer for clarity.)

    public class FeatureRequirement : IAuthorizationRequirement
    {
        public string FeatureName { get; set; }
    
        public FeatureRequirement(string featureName)
        {
            FeatureName = featureName;
        }
    }
    
    /// <summary>
    /// Authorisation based on Feature.
    /// A user is assigned a Role, a role can have N Features.
    /// If the user is not assigned the Feature required to access the Controller Action this will throw an Authorisation Error -
    /// </summary>
    public class FeatureAuthorizationHandler : AuthorizationHandler<FeatureRequirement>
    {
        private readonly IFeaturesProvider _featuresProvider;
        private readonly ICustomUserContext _userContext;
    
        public FeatureAuthorizationHandler([FromServices] IFeaturesProvider featuresProvider,
            [FromServices] ICustomUserContext userContext)
        {
            _featuresProvider = featuresProvider;
            
            _userContext = userContext;
        }
    
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, FeatureRequirement requirement)
        {
            // Authentication of user failed - Means Token expired or Token incorrect BUT Token exists in Header
            if (!context.User.Identity.IsAuthenticated)
            {
                return Task.FromResult(0);
            }
    
            // get the logged in user id and user type
            var userId = _userContext.UserId;
            var userType = _userContext.UserType;
    
            // get features assigned to currently logged in user
            var userFeatures = _featuresProvider.GetFeaturesByUserId(userId, userType);
    
            // check if user has the required feature assigned to it's Role
            if (userFeatures.Select(s => s.Name).Contains(requirement.FeatureName))
                context.Succeed(requirement);
    
            return Task.FromResult(0);
        }
    }
    

    Then in Startup.cs you want to regiseter all your feature/roles as a permission.

    services.AddAuthorization(options =>
    {
        // load all features from DB
        var features = rolesProvider.GetAllFeatures(validationContainer);
    
        // Add policy foreach feature in DB
        foreach (var feature in features)
        {
            options.AddPolicy(feature.Name, policy => policy.Requirements.Add(new FeatureRequirement(feature.Name)));
        }
    });
    

    Also in Startup, register your Depency Injection for the AuthorizationHandler

    services.AddScoped<IAuthorizationHandler, FeatureAuthorizationHandler>();
    

    Then in controller you can just do:

    [Authorize(Policy = "View Asset Driver")]
    public class AssetDriversController : BaseController
    {
    

    where "View Asset Driver" is the name of one of my features.

    OR

    [Authorize(Policy = "View Asset Driver")]
    public async Task<IActionResult> GetAssetDrivers(int companyId, int assetId)
    

    Here is the ICustomUserContext:

       public interface ICustomUserContext
    {
        ClaimsPrincipal CurrentUser { get; }
        int UserId { get; }
        UserTypeEnum UserType { get; }
        int ResellerId { get; }
        int CompanyId { get; }
        int DriverId { get; }
    }
    
     public class CustomUserContextAdapter : ICustomUserContext
        {
            private readonly IHttpContextAccessor _accessor;
    
            public CustomUserContextAdapter(IHttpContextAccessor accessor)
            {
                _accessor = accessor;
            }
    
            public ClaimsPrincipal CurrentUser => _accessor.HttpContext.User;
            public int UserId => CurrentUser != null ? CurrentUser.GetUserIdInternal() : 0;
            public UserTypeEnum UserType => CurrentUser != null ? CurrentUser.GetUserTypeInternal() : UserTypeEnum.User;
            public int ResellerId => CurrentUser != null ? CurrentUser.GetResellerIdInternal() : 0;
            public int CompanyId => CurrentUser != null ? CurrentUser.GetCompanyIdInternal() : 0;
            public int DriverId => CurrentUser != null ? CurrentUser.GetDriverIdInternal() : 0;
        }