TL;DR
- UMA 2.0 lets resource owners control access to their stuff. Think Google Drive sharing, but wired into your authorization server.
- Keycloak.AuthServices now supports the challenge-response flow (automatic ticket-for-RPT exchange) and async approval (request, wait for owner, get in).
- A Blazor Server + Minimal API sample shows the full flow.
UmaTokenHandlerhandles the UMA dance behind the scenes. IKeycloakProtectionClientcovers the permission ticket lifecycle: create, list, approve, deny.
Source code: https://github.com/NikiforovAll/keycloak-authorization-services-dotnet/tree/main/samples/UmaResourceSharing
What is UMA?
Most authorization systems work the same way: the resource server decides who gets access based on roles, claims, or policies that an admin defined. But what if the resource owner, not the admin, should make that call?
User-Managed Access (UMA) is an OAuth 2.0 extension that flips this. Alice can grant or revoke access to her resources for bob, without an admin involved, and without needing to be online when bob asks.
You’ve seen this pattern before:
- Sharing a Google Doc with specific people
- Granting a colleague access to a private GitHub repo
- Approving a permission request in a cloud console
What makes UMA different from regular OAuth is the asynchronous approval step. Bob requests access, alice reviews it later, and only then does bob get in.
Key concepts
| Concept | Description |
|---|---|
| Resource Owner | The user who owns the protected resource |
| Requesting Party | A user who wants access to someone else's resource |
| Permission Ticket | A one-time challenge token representing an access request |
| RPT (Requesting Party Token) | An access token enriched with specific resource permissions |
| Protection API | Keycloak API for managing resources, permissions, and tickets |
Demo
Here’s what this looks like in the Blazor sample.
alice (resource owner), instant access
Alice owns shared-document, so authorization succeeds right away:
bob requests access, alice approves
Bob has no permission. He submits a request, alice approves it, then bob retries and gets in:
bob (no permission), access denied
Without approval, bob gets a clear “Access Denied”:
How the UMA flow works
Two flows, working together.
Challenge-response (instant access)
When a user already has permission, the client handles the exchange automatically:
UmaTokenHandler handles steps 6-9. Your code just makes HTTP calls as usual.
Async approval (request, approve, access)
When a user has no permission, they submit a request. The resource owner reviews it later:
Architecture
Two projects: a Blazor Server client talks to a Minimal API resource server. Keycloak sits in the middle, handling authorization decisions and permission tickets.
Authorization Server] RS[Resource Server
Minimal API] CA[Client App
Blazor Server] end CA -->|HTTP + Bearer Token| RS RS -->|Authorization Check| KC RS -->|Permission Ticket Creation| KC CA -->|Ticket → RPT Exchange| KC
Resource server
The resource server protects endpoints with RequireProtectedResource() and returns WWW-Authenticate: UMA challenges on authorization failure via AddUmaPermissionTicketChallenge():
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddKeycloakWebApi(configuration, options =>
{
options.Audience = "uma-resource-server";
options.RequireHttpsMetadata = false;
});
// Authorization + UMA challenge handler
services.AddAuthorization()
.AddKeycloakAuthorization()
.AddUmaPermissionTicketChallenge();
services.AddAuthorizationServer(configuration)
.AddStandardResilienceHandler();
// Protection API for ticket management
services.AddKeycloakProtectionHttpClient(configuration)
.AddClientCredentialsTokenHandler(tokenClientName);
// Protected endpoints
app.MapGet("/documents/{name}", (string name) =>
new DocumentResponse(name, $"Content of {name}"))
.RequireProtectedResource("shared-document", "read");
app.MapGet("/documents/{name}/details", (string name) =>
new DocumentResponse(name, $"Detailed content of {name}"))
.RequireProtectedResource("shared-document", "write");
It also exposes permission ticket management endpoints:
// Bob submits an access request
app.MapPost("/permissions/request", async (...) =>
{
// ...
var ticket = new PermissionTicket
{
Resource = resourceId, Requester = userId,
ScopeName = "read", Granted = false,
};
await protectionClient.CreatePermissionTicketWithResponseAsync(realm, ticket);
}).RequireAuthorization();
// Alice lists pending requests
app.MapGet("/permissions/pending", async (...) =>
{
return await protectionClient.GetPermissionTicketsAsync(realm,
new GetPermissionTicketsRequestParameters { Granted = false, ReturnNames = true });
}).RequireAuthorization();
// Alice approves
app.MapPut("/permissions/{id}/approve", async (...) =>
{
await protectionClient.UpdatePermissionTicketAsync(realm,
new PermissionTicket { Id = id, Granted = true });
}).RequireAuthorization();
Client app (Blazor Server)
The Blazor client registers UmaTokenHandler from Keycloak.AuthServices.Authorization.Uma. It’s a DelegatingHandler that intercepts 401+UMA responses and handles the ticket exchange:
// UMA ticket exchange client
services.AddKeycloakUmaTicketExchangeHttpClient(configuration);
// HTTP client with UMA handling
services.AddHttpClient("ResourceServer", (sp, client) =>
{
var config = sp.GetRequiredService<IConfiguration>();
var baseUrl = config["services:resource-server:http:0"];
client.BaseAddress = new Uri(baseUrl);
})
.AddUmaTokenHandler();
What UmaTokenHandler does:
- Attaches the user’s access token to outgoing requests
- On
401withWWW-Authenticate: UMA, extracts the permission ticket - Exchanges the ticket for an RPT at Keycloak’s token endpoint
- Retries the original request with the RPT
From the Blazor component side, none of this is visible. You just make HTTP calls:
var response = await Http.GetAsync($"/documents/{documentName}");
if (response.IsSuccessStatusCode)
{
var document = await response.Content.ReadFromJsonAsync<Document>();
}
Keycloak setup
UMA needs a few things configured in Keycloak:
- A confidential client with Authorization Services enabled (the resource server)
- A resource with
ownerManagedAccess: trueand scopes likeread,write - An owner policy that grants the resource owner access
- An OIDC client for user login, with an audience mapper pointing to the resource server
The sample ships with pre-configured Keycloak realm exports, so you can just run it:
dotnet run --project samples/UmaResourceSharing/AppHost
Aspire opens the dashboard automatically with Keycloak, the resource server, and the Blazor app.
Summary
Role-based access control doesn’t cover the case where users share resources with each other. UMA does.
With Keycloak.AuthServices you get UmaTokenHandler for the challenge-response dance, AddUmaPermissionTicketChallenge() for resource servers, and IKeycloakProtectionClient for managing permission tickets. The sample runs with a single dotnet run via Aspire.
References
- Documentation: UMA 2.0
- Documentation: UMA Examples
- Sample source code
- Keycloak Authorization Services Guide
- UMA 2.0 Specification
- Topics:
- dotnet (65) ·
- keycloak (6) ·
- aspnetcore (26) ·
- dotnet (70) ·
- keycloak (8) ·
- auth (7) ·
- uma (1)