Protected Resource Builder
Using Policies is a standard and common approach. However, as the number of resources grows, organizing and managing these policies can become a challenge. To address this issue, we suggest using the Protected Resource Builder approach. This builder provides a convenient way to authorize resources, making it easier to manage and maintain authorization rules.
TIP
💡See Resource Authorization Reference Solution to see a real world example of how to use Protected Resource Builder.
INFO
In most cases, we don't really need to build policies when working with Authorization Server, the authorization responsibility is delegated.
Add to your code
Here is an example of how to migrate from Policies to Protected Resource Builder:
var builder = WebApplication.CreateBuilder(args);
var services = builder.Services;
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddKeycloakWebApi(builder.Configuration);
services
.AddAuthorization()
.AddKeycloakAuthorization()
.AddAuthorizationServer(builder.Configuration);
services.
.AddAuthorizationBuilder()
.AddPolicy(
"WorkspaceRead",
policy => policy.RequireProtectedResource(
resource: "workspaces",
scope: "workspace:read"
)
);
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/workspaces", () => "Hello World!").RequireAuthorization("WorkspaceRead");
app.MapGet("/workspaces", () => "Hello World!")
.RequireProtectedResource("workspaces", "workspace:read");
app.Run();
With just one line, we can authorize access for "workspaces#workspace:read"
, no policy registrations needed. 🚀
Dynamic Resources
You can use path parameters in resource names by enclosing parameter name in '{}'.
In example below, we are substituting resource with value {id}
with the actual value of path parameter.
app.MapGet($"{path}/{{id}}", (string id) => "Hello World!")
.RequireProtectedResource("{id}", "workspace:delete");
NOTE
☝️Currently, it is not possible to use body or query parameters. Please create an issue if it is something that you are interested in adding.
Multiple Scopes
Here is how to check for multiple scopes simultaneously:
app.MapGet(path, Run)
.RequireProtectedResource("workspaces", ["workspace:read", "workspace:delete"]);
Chained calls:
app.MapGet(path, Run)
.RequireProtectedResource("workspaces", "workspace:read")
.RequireProtectedResource("workspaces", "workspace:delete");
Endpoint hierarchy:
var endpoints = app.MapGroup(string.Empty)
.RequireProtectedResource("workspaces", "workspace:read");
// requires workspaces#workspace:read,workspace:delete
endpoints.MapGet(path, Run).RequireProtectedResource("workspaces", "workspace:delete");
// requires workspaces#workspace:read, inherited from parent group
endpoints.MapGet("other-endpoint", Run);
Basically, you can define Group-level protected resources and Endpoint-level protected resources.
NOTE
💡 RequireProtectedResource
is extension method over IEndpointConventionBuilder
. It means you can use it outside of Minimal API. E.g.: MVC, RazorPages, etc. Here is the original design document: https://github.com/NikiforovAll/keycloak-authorization-services-dotnet/issues/87
Multiple Resources
You are not limited to use single resource:
app.MapGet(path, Run)
.RequireProtectedResource("workspaces", "workspace:read")
.RequireProtectedResource("my-workspace", "workspace:delete");
Here is how to define top-level rule for "workspaces" in general and specific rule for particular workspace.
var endpoints = app.MapGroup(string.Empty)
.RequireProtectedResource("workspaces", "workspace:read");
// requires workspaces#workspace:read;my-workspace#workspace:delete
endpoints.MapGet(path, Run).RequireProtectedResource("my-workspace", "workspace:delete");
Ignore Resources
Similarly, to AllowAnonymous
from Microsoft.AspNetCore.Authorization
namespace, there are two methods to ignore what has been registered for protected resources: IgnoreProtectedResources
, IgnoreProtectedResource
.
public static TBuilder AllowAnonymous<TBuilder>(this TBuilder builder) where TBuilder : IEndpointConventionBuilder;
var endpoints = app.MapGroup(string.Empty)
.RequireProtectedResource("workspaces", "workspace:read");
var childrenEndpoints = endpoints
.MapGroup(string.Empty)
.RequireProtectedResource("my-workspace", "workspace:delete");
// requires my-workspace#workspace:read
childrenEndpoints
.MapGet($"{path}1", Run)
.IgnoreProtectedResources()
.RequireProtectedResource("my-workspace", "workspace:read");
// requires my-workspace#workspace:delete,workspace:read
childrenEndpoints
.MapGet($"{path}2", Run)
.IgnoreProtectedResource("workspaces")
.RequireProtectedResource("my-workspace", "workspace:read");
TIP
See the integration tests ProtectedResourcePolicyTests for more details.