DEV Community

Mofajjal Rasul
Mofajjal Rasul

Posted on • Edited on

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

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;
}
Enter fullscreen mode Exit fullscreen mode

Key Design Considerations:

  • Environment Flexibility: The requireValidation parameter enables conditional security enforcement, crucial for development/test environments requiring unrestricted API access
  • Framework Compliance: Implements IAuthorizationRequirement for native integration with ASP.NET Core's policy system
  • Immutability: Read-only property ensures thread-safe operations in concurrent request scenarios

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";
}
Enter fullscreen mode Exit fullscreen mode

Security Strategy:

  • Principle of Least Privilege: Explicit allow-list approach minimizes attack surface
  • Maintainability: Centralized declaration simplifies security audits and modifications
  • Case Insensitivity: Contains-check comparison prevents route casing vulnerabilities

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

Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuration Strategy:

  • Policy Composition: Combines JWT authentication with custom requirements
  • Environment Adaptation: Configures validation toggle via appsettings.json
  • Scoped Handler Registration: Ensures proper dependency lifecycle management
  • Default Policy Enforcement: Applies uniform security rules across all endpoints

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.

Top comments (0)