reverse-proxy icon indicating copy to clipboard operation
reverse-proxy copied to clipboard

Support `AuthorizationRoles` in RouteConfig to Avoid Policy Explosion

Open gumbarros opened this issue 5 months ago • 3 comments

Current Situation

When configuring routes dynamically from the database, I’m using this pattern:

routes.Add(new RouteConfig
{
    RouteId = $"{route.AppId}_{route.Id}",
    ClusterId = clusterId,
    Match = new RouteMatch
    {
        Path = routePath,
        QueryParameters = queryParameters.Count > 0 ? queryParameters : null
    },
    AuthorizationPolicy = RoutePrefix + route.Id,
    Transforms = transforms
});

This forces me to create one authorization policy per route. In our case, we have ~300 routes, resulting in:

  • 300 custom authorization policies
  • 300 DB calls on startup to resolve those policies

Proposed Change

It would be significantly more efficient if I could instead define roles directly in the route config like this:

routes.Add(new RouteConfig
{
    RouteId = $"{route.AppId}_{route.Id}",
    ClusterId = clusterId,
    Match = new RouteMatch
    {
        Path = routePath,
        QueryParameters = queryParameters.Count > 0 ? queryParameters : null
    },
    AuthorizationRoles = route.Roles.Select(r => r.Name),
    Transforms = transforms
});

It made sense in my head, just add at ProxyEndpointFactory this snippet:

else if (config.AuthorizationRoles != null && config.AuthorizationRoles.Length > 0)
{
    endpointBuilder.Metadata.Add(new AuthorizeAttribute
    {
        Roles = string.Join(",", config.AuthorizationRoles)
    });
}

Why This Matters

  • Reduces the need to define and register hundreds of policies dynamically
  • Simplifies route configuration and improves maintainability
  • Aligns with how [Authorize(Roles = "...")] already works in controllers and endpoints

If there’s a way supported by YARP for role-based auth at the route level (without needing per-route policies), I’m open to it. I checked the Metadata prop, but idk how to consume it at a single authorization policy.

gumbarros avatar Aug 03 '25 13:08 gumbarros

Possible related and/or duplicate issue:

  • https://github.com/dotnet/yarp/issues/1370

MihuBot avatar Aug 03 '25 13:08 MihuBot

For anyone interessed, I did the following workaround, but would be more elegant if YARP has it built-in:

routes.Add(new RouteConfig
{
    RouteId = $"{route.AppId}_{route.Id}",
    ClusterId = clusterId,
    Match = new RouteMatch
    {
        Path = routePath,
        QueryParameters = queryParameters.Count > 0 ? queryParameters : null
    },
    AuthorizationPolicy = "ReverseProxyRolePolicy",
    Metadata = new Dictionary<string, string>
    {
        { "Roles", string.Join(',',route.Roles.Select(r=>r.Name)) }
    },
    Transforms = transforms
});
using Yarp.ReverseProxy.Model;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;

namespace JJConsulting.Infinity.Lib.ReverseProxy.Authorization;

public sealed class ReverseProxyAuthorizationHandler : AuthorizationHandler<ReverseProxyRolesRequirement>
{
    private const string AdminRole = "Admin";

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ReverseProxyRolesRequirement requirement)
    {
        if (context.Resource is not HttpContext httpContext)
            return Task.CompletedTask;

        var user = httpContext.User;
        
        if (user.IsInRole(AdminRole))
            return Success(context, requirement);

        var endpoint = httpContext.GetEndpoint();
        var routeModel = endpoint?.Metadata.GetMetadata<RouteModel>();

        if (routeModel is null)
            return Failure(context);

        var isAuthenticated = user.Identity?.IsAuthenticated == true;
        
        if (routeModel.Config.Metadata is null || routeModel.Config.Metadata.Count == 0)
        {
            return isAuthenticated ? Success(context, requirement) : Failure(context);
        }

        var hasRoles = routeModel.Config.Metadata.TryGetValue("Roles", out var rolesString);
        
        if (!hasRoles || string.IsNullOrEmpty(rolesString))
        {
            return isAuthenticated ? Success(context, requirement) : Failure(context);
        }
        
        var roles = rolesString.Split(',');
        
        if (isAuthenticated && roles.Any(role => user.IsInRole(role)))
            return Success(context, requirement);

        return Failure(context);
    }

    private static Task Failure(AuthorizationHandlerContext context)
    {
        context.Fail();
        
        return Task.CompletedTask;
    }

    private static Task Success(AuthorizationHandlerContext context, ReverseProxyRolesRequirement requirement)
    {
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

gumbarros avatar Aug 03 '25 14:08 gumbarros

A suggestion related to #1370, but nuclear breaking change, would be a JSON representation of the AuthorizeAttribute

{
  "ReverseProxy": {
    "Routes": [
      {
        "RouteId": "authRoute",
        "ClusterId": "authCluster",
        "Match": {
          "Path": "/api/secure/{**catch-all}"
        },
        "Authorize": {
          "Policy": "MyCustomPolicy",
          "Roles": ["Admin","Manager"],
          "AuthenticationSchemes": ["Cookies","Bearer"]
        }
      }
    ],
    "Clusters": {
      "authCluster": {
        "Destinations": {
          "authDestination": {
            "Address": "https://my-secure-api.internal/"
          }
        }
      }
    }
  }
}

gumbarros avatar Aug 03 '25 19:08 gumbarros