API Protection
This page explains how the Platform API enforces authorization using Keycloak Authorization Services. The API delegates all access decisions to Keycloak, keeping authorization logic out of application code.
Setup
Authorization is wired up in AuthorizationConfiguration.cs:
public static IServiceCollection AddPlatformAuthorization(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddKeycloakAuthorization()
.AddAuthorizationServer(configuration);
services.AddAuthorizationBuilder()
.AddFallbackPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
return services;
}This does two things:
AddKeycloakAuthorization().AddAuthorizationServer(configuration)- Registers Keycloak as the authorization server. The API will call Keycloak's token endpoint to evaluate permissions at runtime. Configuration is read from theKeycloaksection inappsettings.jsonor environment variables.Fallback policy - Any endpoint that doesn't declare a specific permission still requires an authenticated user. No anonymous access is allowed.
The method is called in Program.cs:
builder.Services.AddPlatformAuthorization(builder.Configuration);Configuration
The API requires Keycloak connection settings (server URL, realm, client ID, and client secret). In local development, the .NET Aspire AppHost passes these automatically. For standalone deployments, see Provisioning.
The Permission Type
Permissions are modeled as a record struct in Permission.cs, pairing a resource name with a scope. The PolicyName property (e.g., "gateways:read") is used internally by the Keycloak authorization middleware. An implicit string conversion means Permission values can be used anywhere a policy name string is expected.
Defining Permissions
All permissions are defined as static fields in Permissions.cs, organized by resource (e.g., Permissions.Gateways.Read, Permissions.Assets.Write). Each permission corresponds to a resource + scope combination that must also exist in the Keycloak configuration. The full set can be enumerated via Permissions.All.
Protecting Endpoints
Use RequirePermission() on minimal API endpoints:
app.MapGet("/api/gateways", GetGateways)
.RequirePermission(Permissions.Gateways.Read);
app.MapPost("/api/gateways", CreateGateway)
.RequirePermission(Permissions.Gateways.Write);
app.MapDelete("/api/gateways/{id}", DeleteGateway)
.RequirePermission(Permissions.Gateways.Delete);For endpoints requiring multiple permissions, use RequireAllPermissions():
app.MapPost("/api/workflows/{id}/execute", ExecuteWorkflow)
.RequireAllPermissions(Permissions.Workflow.Write, Permissions.Assets.Read);Extension Methods
The extension methods in EndpointAuthorizationExtensions.cs wrap RequireProtectedResource() from the Keycloak.AuthServices.Authorization package. At request time, this sends a permission evaluation request to Keycloak's token endpoint using the UMA (User-Managed Access) protocol.
How It Works at Runtime
When a request hits a protected endpoint:
- The ASP.NET Core authorization middleware extracts the bearer token from the request
- For each
RequireProtectedResourcerequirement, it sends an RPT (Requesting Party Token) request to Keycloak's token endpoint using the UMA grant type - Keycloak evaluates the request against its configured policies and permissions
- If access is granted, the request proceeds; otherwise, a
403 Forbiddenis returned
Performance consideration
Each permission check results in a call to Keycloak. The Keycloak.AuthServices library handles token caching to minimize overhead, but be mindful when using RequireAllPermissions() with many permissions on a single endpoint.
Adding a New Permission
See Provisioning for the end-to-end workflow covering both Keycloak configuration and API code.
Important
Permissions must match between the API code and the Keycloak configuration. If you add a RequirePermission() call for a resource/scope that doesn't exist in Keycloak, all requests to that endpoint will be denied.