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:
- 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";
}
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;
});
}
}
Critical Functionality:
- Environment Bypass: Conditional validation skip ensures development agility
- Route Deconstruction:
- Extracts
endpointRoute
using standardized path segment indexing - Parses JWT claim
ClaimTypes.Role
for permission lookup
- Extracts
- Dynamic Permission Check:
- Utilizes unit-of-work pattern for database abstraction
- Async query prevents thread-blocking during permission validation
- Defensive Programming:
- Comprehensive try-catch with structured logging
- Explicit failure states for auditability
- Reflection-Based Whitelisting:
- Dynamically checks against
OpenHttpEndpoints
constants - Eliminates hardcoded path comparisons
- Dynamically checks against
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:
- 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.