Support `AuthorizationRoles` in RouteConfig to Avoid Policy Explosion
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.
Possible related and/or duplicate issue:
- https://github.com/dotnet/yarp/issues/1370
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;
}
}
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/"
}
}
}
}
}
}