Custom Role-Based Authorization with JWT in ASP.NET Core

Published on Nov 3, 2024 | Reading time: 3 min


This implementation demonstrates a robust authorization system for ASP.NET Core applications, leveraging JWT authentication to enforce dynamic role-based access control (RBAC). The solution provides granular permission management while maintaining strict security protocols. Below is the complete technical breakdown with unmodified code samples and professional analysis.


1. Custom Authorization Requirement Implementation

Purpose: Establish conditional validation logic for environment-specific security policies

public class AccessRequirement(bool requireValidation) : IAuthorizationRequirement
{
    public bool NeedsValidation { get; } = requireValidation;
}

Key Design Considerations:


2. Global Route Whitelist Configuration

Purpose: Define publicly accessible endpoints exempt from authorization checks

public static class OpenHttpEndpoints
{
    public const string Authenticate = "Authenticate"; 
    public const string FetchUserDataByKey = "FetchUserDataByKey";
}

Security Strategy:


3. Core Authorization Handler

Purpose: Execute role-permission validation against requested API routes

public class AccessControlHandler : AuthorizationHandler<AccessRequirement>
{
    private readonly IDataRepository _dataRepository;
    private readonly ILogger<AccessControlHandler> _logService;

    public AccessControlHandler(
        IDataRepository dataRepository,
        ILogger<AccessControlHandler> logService
        )
    {
        _dataRepository = dataRepository;
        _logService = logService;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AccessRequirement requirement)
    {
        try
        {
            if (!requirement.NeedsValidation ||
                context.Resource is not HttpContext requestContext ||
                IsOpenEndpoint(requestContext.Request.Path.Value))
            {
                context.Succeed(requirement);
                return;
            }

            if (!ExtractEndpointDetails(requestContext, out var endpointRoute, out var roleId))
            {
                context.Fail();
                return;
            }

            var hasAccess = await _dataRepository.Permissions.CheckAccess(roleId, endpointRoute);
            if (!hasAccess)
            {
                context.Fail();
                return;
            }

            context.Succeed(requirement);
        }
        catch (Exception ex)
        {
            _logService.LogError(ex, "Authorization check encountered an error");
            context.Fail();
        }
    }

    private static bool ExtractEndpointDetails(HttpContext requestContext, out string endpointRoute, out long roleId)
    {
        endpointRoute = string.Empty;
        roleId = 0;

        var requestPath = requestContext.Request.Path.Value;
        if (string.IsNullOrEmpty(requestPath) ||
            !long.TryParse(requestContext.User.FindFirstValue(ClaimTypes.Role), out roleId))
        {
            return false;
        }

        var pathSegments = requestPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
        endpointRoute = pathSegments.Length >= 3 ? $"{pathSegments.ElementAtOrDefault(1)}/{pathSegments.ElementAtOrDefault(2)}" : string.Empty;

        return !string.IsNullOrEmpty(endpointRoute);
    }

    private static bool IsOpenEndpoint(string? path)
    {
        if (string.IsNullOrEmpty(path)) return true;

        FieldInfo[] fields = typeof(OpenHttpEndpoints)
            .GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.GetField);

        return Array.Exists(fields, field =>
        {
            if (field.FieldType == typeof(string))
            {
                return field.GetValue(null) is string fieldValue &&
                        path.Contains(fieldValue, StringComparison.OrdinalIgnoreCase);
            }
            return false;
        });
    }
}

Critical Functionality:

  1. Environment Bypass: Conditional validation skip ensures development agility
  2. Route Deconstruction:
    • Extracts endpointRoute using standardized path segment indexing
    • Parses JWT claim ClaimTypes.Role for permission lookup
  3. Dynamic Permission Check:
    • Utilizes unit-of-work pattern for database abstraction
    • Async query prevents thread-blocking during permission validation
  4. Defensive Programming:
    • Comprehensive try-catch with structured logging
    • Explicit failure states for auditability
  5. Reflection-Based Whitelisting:
    • Dynamically checks against OpenHttpEndpoints constants
    • Eliminates hardcoded path comparisons

4. Security Policy Configuration

Purpose: Integrate custom authorization into ASP.NET Core pipeline

public static class AccessControlService
{
    public static IServiceCollection ConfigureAccessControl(this IServiceCollection services, IConfiguration config)
    {
        var validationRequired = config.GetValue("EnableRoleBasedAccess", true);

        services.AddScoped<IAuthorizationHandler, AccessControlHandler>();

        services.AddAuthorizationBuilder()
            .SetDefaultPolicy(new AuthorizationPolicyBuilder()
                .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
                .RequireAuthenticatedUser()
                .AddRequirements(new AccessRequirement(validationRequired))
                .Build());

        return services;
    }
}

Configuration Strategy:


This implementation provides a foundation for enterprise authorization systems while maintaining strict compliance with original code requirements. The unmodified code blocks ensure compatibility with existing JWT authentication flows while enabling seamless permission management evolution.