Search code examples
asp.net-corekeycloakroles

ASP.NET Core 8 + Keycloak: Role-based Authorization for Derived Controllers without Code Duplication


I'm working with ASP.NET Core 8 and Keycloak (version 24) for authentication and role-based access control.

Scenario: I have a base controller that implements basic CRUD operations for multiple object types, e.g.:

public class BaseController<T> : Controller
{
    [HttpGet]
    public virtual async Task<IActionResult> Get()
    {
        // Base implementation
    }

    // Other CRUD methods
}

For each specific object type, I have a derived controller that only provides the CRUD operations for that specific type, e.g.:

public class ObjectAController : BaseController<ObjectA>
{
    // Inherits CRUD operations from BaseController
}

Now, I’ve introduced Keycloak roles like Read_ObjectA, Read_ObjectB, Write_ObjectA, etc.

Problem: I need to enforce authorization for each object type with its respective role. For example, ObjectAController should allow access only to users with the Read_ObjectA or Write_ObjectA roles. However, I want to avoid duplicating the methods in each derived controller just to apply the [Authorize] attribute.

Question: Is there a way to apply authorization generically in the base controller so that I don't have to duplicate methods in each derived controller just to specify the correct role? Would writing a custom authorization service be a better solution here, and if so, how could I implement this cleanly?

A simple but tedious approach would be something like this in the derived controller:
[Authorize(Roles = "Read_ObjectA")]
[HttpGet]
public override async Task<IActionResult> Get()
{
    return await base.Get();
}

This feels redundant since I'm only adding the [Authorize] attribute in each derived controller, while the logic remains in the base controller.


Solution

  • I think you just need implement CustomAuthorizeAttribute

        public class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter
        {
            private readonly string[] _roles;
    
            public CustomAuthorizeAttribute(params string[] roles)
            {
                _roles = roles;
            }
    
            public void OnAuthorization(AuthorizationFilterContext context)
            {
                // Get the controller name
                var controllerName = context.RouteData.Values["controller"]?.ToString();
    
                var user = context.HttpContext.User;
                var userRoles = user.Claims.Where(c => c.Type == "role").Select(c => c.Value);
    
                // Recombine the "Read" to "Read_ObjectA"
                if (!_roles.Any(role => userRoles.Contains(role+"_"+ controllerName)))
                {
                    context.Result = new ForbidResult(); // User is not authorized
                }
            }
        }
    

    Then Use like

            [CustomAuthorize("Read")]
            //[CustomAuthorize("Read","Write")]
            [HttpGet]
            public virtual async Task<IActionResult> Get()
            {
                // Base implementation
            }