UMA (User-Managed Access) 2.0 is an OAuth 2.0-based protocol that enables resource owners to control access to their protected resources. Unlike standard OAuth 2.0 where the resource owner and requesting party are typically the same, UMA introduces the ability to distinguish between:
- Resource Owner – The entity that owns and controls access to resources
- Requesting Party – The entity seeking access to protected resources
- Authorization Server – Manages permissions and policies (Keycloak)
- Resource Server – Hosts the protected resources
This enables scenarios like:
- Document sharing between users
- Multi-domain resource management
- Centralized authorization policies
- User-controlled permission delegation
UMA vs OAuth 2.0
Architecture
Keycloak Authorization Services Components
Keycloak implements the full UMA 2.0 specification through its Authorization Services:
UMA Endpoints
Keycloak exposes UMA endpoints through the Protection API:
| Endpoint | Path | Purpose |
|---|---|---|
| Resource Registration | /realms/{realm}/authz/protection/resource_set |
Register/manage protected resources |
| Permission | /realms/{realm}/authz/protection/permission |
Create permission tickets |
| Permission Ticket | /realms/{realm}/authz/protection/permission/ticket |
Manage permission tickets |
| UMA Policy | /realms/{realm}/authz/protection/uma-policy |
User-managed permission policies |
| Token | /realms/{realm}/protocol/openid-connect/token |
Exchange tickets for RPT |
Source Code: services/src/main/java/org/keycloak/authorization/protection/ProtectionService.java
@Path("/resource_set")
public Object resource() {
KeycloakIdentity identity = createIdentity(true);
ResourceServer resourceServer = getResourceServer(identity);
return new ResourceService(this.session, resourceServer, identity, resourceManager);
}
@Path("/permission")
public Object permission() {
KeycloakIdentity identity = createIdentity(false);
return new PermissionService(identity, getResourceServer(identity), this.authorization);
}
@Path("/permission/ticket")
public Object ticket() {
KeycloakIdentity identity = createIdentity(false);
return new PermissionTicketService(identity, getResourceServer(identity), this.authorization);
}
@Path("/uma-policy")
public Object policy() {
KeycloakIdentity identity = createIdentity(false);
return new UserManagedPermissionService(identity, getResourceServer(identity),
this.authorization, createAdminEventBuilder(identity, getResourceServer(identity)));
}
UMA Flow
Complete UMA 2.0 Authorization Flow
Alternative: Permission Request Flow (submit_request)
When a requesting party doesn’t have permission, they can submit a request to the resource owner:
Core Concepts
Resources
A resource represents a protected entity (document, API endpoint, photo, etc.):
Source Code: server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java
public interface Resource {
String getId();
String getName();
String getDisplayName();
Set<string> getUris(); // URIs that identify this resource
String getType(); // Resource type for grouping
List<scope> getScopes(); // Available scopes
String getOwner(); // Resource owner ID
boolean isOwnerManagedAccess(); // UMA: owner controls access
ResourceServer getResourceServer();
Map<string, list<string="">> getAttributes();
}
Resource Representation (UMA format):
// services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java
public class UmaResourceRepresentation extends ResourceRepresentation {
@JsonProperty("resource_scopes") // UMA-compliant property name
@Override
public Set<scoperepresentation> getScopes() {
return super.getScopes();
}
}
Scopes
Scopes define the actions that can be performed on a resource:
public class ScopeRepresentation {
private String id;
private String name; // e.g., "read", "write", "delete"
private String displayName;
private String iconUri;
}
Permission Tickets
A permission ticket is a token that represents a set of requested permissions:
Source Code: core/src/main/java/org/keycloak/representations/idm/authorization/PermissionTicketToken.java
public class PermissionTicketToken extends JsonWebToken {
private final List<permission> permissions;
private Map<string, list<string="">> claims;
public PermissionTicketToken(List<permission> permissions,
String audience,
AccessToken accessToken) {
if (accessToken != null) {
id(TokenIdGenerator.generateId());
subject(accessToken.getSubject());
this.exp(accessToken.getExp());
this.nbf(accessToken.getNbf());
iat(accessToken.getIat());
issuedFor(accessToken.getIssuedFor());
}
if (audience != null) {
audience(audience);
}
this.permissions = permissions;
}
}
Requesting Party Token (RPT)
The RPT is an access token that contains the granted permissions:
// The RPT contains an "authorization" claim with permissions AccessToken rpt = responseBuilder.getAccessToken(); Authorization authorization = new Authorization(); authorization.setPermissions(entitlements); rpt.setAuthorization(authorization);
RPT Structure:
"exp": 1234567890,
"iat": 1234567800,
"jti": "token-id",
"iss": "https://keycloak.example.com/realms/myrealm",
"sub": "user-id",
"typ": "Bearer",
"azp": "my-client",
"authorization": {
"permissions": [
{
"rsid": "resource-id",
"rsname": "My Document",
"scopes": ["read", "write"]
}
]
}
}
Permission Model
Source Code: server-spi-private/src/main/java/org/keycloak/authorization/model/PermissionTicket.java
public interface PermissionTicket {
enum FilterOption {
ID, RESOURCE_ID, RESOURCE_NAME, SCOPE_ID, SCOPE_IS_NULL,
OWNER, GRANTED, REQUESTER, REQUESTER_IS_NULL,
POLICY_IS_NOT_NULL, POLICY_ID
}
String getId();
String getOwner(); // Resource owner
String getRequester(); // Who is requesting access
Resource getResource(); // The protected resource
Scope getScope(); // Requested scope
boolean isGranted(); // Whether permission is granted
Long getCreatedTimestamp();
Long getGrantedTimestamp();
void setGrantedTimestamp(Long millis);
ResourceServer getResourceServer();
Policy getPolicy(); // Associated UMA policy
void setPolicy(Policy policy);
}
UMA Grant Type
Keycloak implements the UMA grant type for exchanging permission tickets for RPTs:
Grant Type: urn:ietf:params:oauth:grant-type:uma-ticket
Source Code: services/src/main/java/org/keycloak/protocol/oidc/grants/PermissionGrantType.java
public class PermissionGrantType extends OAuth2GrantTypeBase {
@Override
public Response process(Context context) {
// Extract optional Bearer token for public clients
String accessTokenString = extractAuthorizationHeaderToken(headers);
// Handle claim_token for additional claims
String claimToken = formParams.get("claim_token");
String claimTokenFormat = formParams.getFirst("claim_token_format");
// Build authorization request
KeycloakAuthorizationRequest authorizationRequest =
new KeycloakAuthorizationRequest(
session.getProvider(AuthorizationProvider.class),
tokenManager, event, this.request, cors, clientConnection);
// Set request parameters
authorizationRequest.setTicket(formParams.getFirst("ticket"));
authorizationRequest.setClaimToken(claimToken);
authorizationRequest.setClaimTokenFormat(claimTokenFormat);
authorizationRequest.setPct(formParams.getFirst("pct"));
// Handle existing RPT for permission upgrade
String rpt = formParams.getFirst("rpt");
if (rpt != null) {
AccessToken accessToken = session.tokens().decode(rpt, AccessToken.class);
authorizationRequest.setRpt(accessToken);
}
// Handle direct permission requests
List<string> permissions = formParams.get("permission");
if (permissions != null) {
authorizationRequest.addPermissions(permissions,
permissionResourceFormat, matchingUri, maxResults);
}
// Set response metadata
Metadata metadata = new Metadata();
metadata.setIncludeResourceName(includeResourceName);
metadata.setLimit(responsePermissionsLimit);
metadata.setResponseMode(formParams.getFirst("response_mode"));
authorizationRequest.setMetadata(metadata);
// Evaluate and return RPT
return AuthorizationTokenService.instance().authorize(authorizationRequest);
}
}
Protection API
Resource Registration
Endpoint: POST /realms/{realm}/authz/protection/resource_set
Source Code: services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@POST
@Consumes("application/json")
@Produces("application/json")
public Response create(UmaResourceRepresentation resource) {
ResourceRepresentation newResource = resourceManager.create(resource);
return Response.status(Status.CREATED)
.entity(new UmaResourceRepresentation(newResource))
.build();
}
@Path("{id}")
@PUT
@Consumes("application/json")
public Response update(@PathParam("id") String id, ResourceRepresentation resource) {
// Update existing resource
}
@Path("/{id}")
@DELETE
public Response delete(@PathParam("id") String id) {
// Delete resource
}
@GET
@Produces("application/json")
public Response find(@QueryParam("_id") String id,
@QueryParam("name") String name,
@QueryParam("uri") String uri,
@QueryParam("owner") String owner,
@QueryParam("type") String type,
@QueryParam("scope") String scope,
@QueryParam("matchingUri") Boolean matchingUri,
@QueryParam("exactName") Boolean exactName,
@QueryParam("deep") Boolean deep,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
// Query resources
}
Permission Ticket Creation
Endpoint: POST /realms/{realm}/authz/protection/permission
Source Code: services/src/main/java/org/keycloak/authorization/protection/permission/AbstractPermissionService.java
public Response create(List<permissionrequest> request) {
// Validate requested resources and scopes
List<permission> requestedResources = verifyRequestedResource(request);
// Create permission ticket token
String audience = Urls.realmIssuer(session.getContext().getUri().getBaseUri(),
realm.getName());
PermissionTicketToken token = new PermissionTicketToken(permissions, audience,
this.identity.getAccessToken());
// Add claims if provided
Map<string, list<string="">> claims = new HashMap<>();
for (PermissionRequest permissionRequest : request) {
Map<string, list<string="">> requestClaims = permissionRequest.getClaims();
if (requestClaims != null) {
claims.putAll(requestClaims);
}
}
if (!claims.isEmpty()) {
token.setClaims(claims);
}
// Encode and return ticket
String ticketToken = authorization.getKeycloakSession().tokens().encode(token);
return Response.status(Status.CREATED)
.entity(new PermissionResponse(ticketToken))
.build();
}
Permission Ticket Management
Endpoint: /realms/{realm}/authz/protection/permission/ticket
Source Code: services/src/main/java/org/keycloak/authorization/protection/permission/PermissionTicketService.java
@POST
@Consumes("application/json")
@Produces("application/json")
public Response create(PermissionTicketRepresentation representation) {
// Create a persistent permission ticket
PermissionTicket ticket = ticketStore.create(
resourceServer, resource, scope, user.getId());
if (representation.isGranted()) {
ticket.setGrantedTimestamp(System.currentTimeMillis());
}
return Response.ok(ModelToRepresentation.toRepresentation(ticket,
authorization, returnNames)).build();
}
@PUT
@Consumes("application/json")
public Response update(PermissionTicketRepresentation representation) {
// Update permission ticket (grant/revoke)
// Only owner or resource server can update
}
@Path("{id}")
@DELETE
public Response delete(@PathParam("id") String id) {
// Delete permission ticket
// Owner, requester, or resource server can delete
}
@GET
@Produces("application/json")
public Response find(@QueryParam("scopeId") String scopeId,
@QueryParam("resourceId") String resourceId,
@QueryParam("owner") String owner,
@QueryParam("requester") String requester,
@QueryParam("granted") Boolean granted,
@QueryParam("returnNames") Boolean returnNames,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResult) {
// Query permission tickets
}
User-Managed Permissions (UMA Policies)
Resource owners can create policies to grant access to their resources:
Endpoint: /realms/{realm}/authz/protection/uma-policy
Source Code: services/src/main/java/org/keycloak/authorization/protection/policy/UserManagedPermissionService.java
@POST
@Path("{resourceId}")
@Consumes("application/json")
public Response create(@PathParam("resourceId") String resourceId,
UmaPermissionRepresentation representation) {
// Resource owner creates a UMA policy
checkRequest(resourceId, representation);
representation.addResource(resourceId);
representation.setOwner(identity.getId());
return findById(delegate.create(representation).getId());
}
@Path("{policyId}")
@PUT
@Consumes("application/json")
public Response update(@PathParam("policyId") String policyId, String payload) {
// Updates UMA policy
checkRequest(getAssociatedResourceId(policyId), representation);
return delegate.getResource(policyId).update(payload);
}
@Path("{policyId}")
@DELETE
public Response delete(@PathParam("policyId") String policyId) {
// Deletes UMA policy
}
@GET
@Produces("application/json")
public Response find(@QueryParam("name") String name,
@QueryParam("resource") String resource,
@QueryParam("scope") String scope) {
// Finds UMA policies owned by the identity
return delegate.findAll(null, name, "uma", null, resource, scope,
true, identity.getId(), null, firstResult, maxResult);
}
UMA Permission Representation:
// core/src/main/java/org/keycloak/representations/idm/authorization/UmaPermissionRepresentation.java
public class UmaPermissionRepresentation extends AbstractPolicyRepresentation {
@Override
public String getType() {
return "uma";
}
private Set<string> roles; // Allowed roles
private Set<string> groups; // Allowed groups
private Set<string> clients; // Allowed clients
private Set<string> users; // Allowed users
private String condition; // Condition script
}
Policy Evaluation
UMA Policy Provider
Source Code: authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/permission/UMAPolicyProvider.java
public class UMAPolicyProvider extends AbstractPermissionProvider {
@Override
public void evaluate(Evaluation evaluation) {
ResourcePermission permission = evaluation.getPermission();
Resource resource = permission.getResource();
if (resource != null) {
Identity identity = evaluation.getContext().getIdentity();
// Resource owner always has access
if (resource.getOwner().equals(identity.getId())) {
evaluation.grant();
return;
}
}
// Evaluate associated policies
super.evaluate(evaluation);
}
}
Permission Ticket Aware Decision Collector
Handles UMA-specific decision logic including permission request submission:
Source Code: server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/PermissionTicketAwareDecisionResultCollector.java
public class PermissionTicketAwareDecisionResultCollector
extends DecisionPermissionCollector {
@Override
protected void onGrant(Permission grantedPermission) {
// Remove granted permissions from ticket
List<permission> permissions = ticket.getPermissions();
Iterator<permission> itPermissions = permissions.iterator();
while (itPermissions.hasNext()) {
Permission permission = itPermissions.next();
if (permission.getResourceId() == null ||
permission.getResourceId().equals(grantedPermission.getResourceId())) {
Set<string> scopes = permission.getScopes();
Iterator<string> itScopes = scopes.iterator();
while (itScopes.hasNext()) {
if (grantedPermission.getScopes().contains(itScopes.next())) {
itScopes.remove();
}
}
if (scopes.isEmpty()) {
itPermissions.remove();
}
}
}
}
@Override
public void onComplete() {
super.onComplete();
// Handle submit_request: create permission tickets for denied permissions
if (request.isSubmitRequest()) {
List<permission> permissions = ticket.getPermissions();
if (permissions != null) {
for (Permission permission : permissions) {
Resource resource = resourceStore.findById(
resourceServer, permission.getResourceId());
// Only create tickets for owner-managed resources
if (resource != null &&
resource.isOwnerManagedAccess() &&
!resource.getOwner().equals(identity.getId()) &&
!resource.getOwner().equals(resourceServer.getClientId())) {
Set<string> scopes = permission.getScopes();
if (scopes.isEmpty()) {
ticketStore.create(resourceServer, resource,
null, identity.getId());
} else {
for (String scopeId : scopes) {
Scope scope = scopeStore.findByName(
resourceServer, scopeId);
ticketStore.create(resourceServer, resource,
scope, identity.getId());
}
}
}
}
}
}
}
}
Resource Server Configuration
Enabling Authorization Services
To use UMA, enable Authorization Services on a client:
Source Code: core/src/main/java/org/keycloak/representations/idm/authorization/ResourceServerRepresentation.java
public class ResourceServerRepresentation {
private String id;
private String clientId;
private String name;
// Enable Protection API for remote resource management
private boolean allowRemoteResourceManagement = true;
// Policy enforcement mode
private PolicyEnforcementMode policyEnforcementMode =
PolicyEnforcementMode.ENFORCING;
private List<resourcerepresentation> resources;
private List<policyrepresentation> policies;
private List<scoperepresentation> scopes;
private DecisionStrategy decisionStrategy;
}
Policy Enforcement Modes
public enum PolicyEnforcementMode {
ENFORCING, // Requests denied by default (require explicit permission)
PERMISSIVE, // Requests allowed by default
DISABLED // No enforcement
}
Practical Usage Examples
1. Setup: Get Protection API Token (PAT)
The Protection API requires a token with the uma_protection scope:
# Get PAT for the resource server PAT=$(curl -s -X POST \ "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=my-resource-server" \ -d "client_secret=secret" | jq -r '.access_token') echo "PAT: $PAT"
2. Register a Protected Resource
# Register a resource
RESOURCE=$(curl -s -X POST \
"http://localhost:8080/realms/myrealm/authz/protection/resource_set" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d '{
"name": "My Document",
"displayName": "My Important Document",
"type": "document",
"uris": ["/documents/123"],
"resource_scopes": [
{"name": "read"},
{"name": "write"},
{"name": "delete"}
],
"ownerManagedAccess": true
}')
RESOURCE_ID=$(echo $RESOURCE | jq -r '._id')
echo "Resource ID: $RESOURCE_ID"
3. List Resources
# List all resources curl -s "http://localhost:8080/realms/myrealm/authz/protection/resource_set" \ -H "Authorization: Bearer $PAT" | jq # Get specific resource curl -s "http://localhost:8080/realms/myrealm/authz/protection/resource_set/$RESOURCE_ID" \ -H "Authorization: Bearer $PAT" | jq # Search by name curl -s "http://localhost:8080/realms/myrealm/authz/protection/resource_set?name=My%20Document" \ -H "Authorization: Bearer $PAT" | jq
4. Create Permission Ticket (Resource Server)
When a client tries to access a resource without authorization:
# Create permission ticket for specific resource and scopes
TICKET_RESPONSE=$(curl -s -X POST \
"http://localhost:8080/realms/myrealm/authz/protection/permission" \
-H "Authorization: Bearer $PAT" \
-H "Content-Type: application/json" \
-d "[{
\"resource_id\": \"$RESOURCE_ID\",
\"resource_scopes\": [\"read\"]
}]")
PERMISSION_TICKET=$(echo $TICKET_RESPONSE | jq -r '.ticket')
echo "Permission Ticket: $PERMISSION_TICKET"
5. Exchange Ticket for RPT (Requesting Party)
# Get user token first USER_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password" \ -d "client_id=my-client" \ -d "username=alice" \ -d "password=password" | jq -r '.access_token') # Exchange permission ticket for RPT RPT_RESPONSE=$(curl -s -X POST \ "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ -d "ticket=$PERMISSION_TICKET" \ -d "client_id=my-client" \ -H "Authorization: Bearer $USER_TOKEN") RPT=$(echo $RPT_RESPONSE | jq -r '.access_token') echo "RPT: $RPT"
6. Request RPT Directly (Without Ticket)
# Request specific permissions directly RPT_RESPONSE=$(curl -s -X POST \ "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ -d "audience=my-resource-server" \ -d "permission=$RESOURCE_ID#read" \ -d "client_id=my-client" \ -H "Authorization: Bearer $USER_TOKEN") # Request all available permissions RPT_RESPONSE=$(curl -s -X POST \ "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ -d "audience=my-resource-server" \ -d "client_id=my-client" \ -H "Authorization: Bearer $USER_TOKEN")
7. Submit Permission Request (Async Approval)
# Request permission with submit_request flag
RESPONSE=$(curl -s -X POST \
"http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=my-resource-server" \
-d "permission=$RESOURCE_ID#write" \
-d "submit_request=true" \
-d "client_id=my-client" \
-H "Authorization: Bearer $USER_TOKEN")
# Response will be either:
# - RPT if permission granted
# - {"error": "request_submitted"} if request created for owner approval
# - {"error": "access_denied"} if request denied
8. Resource Owner: View Pending Requests
# Get owner's token OWNER_TOKEN=$(curl -s -X POST \ "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=password" \ -d "client_id=my-client" \ -d "username=bob" \ -d "password=password" | jq -r '.access_token') # List pending permission requests curl -s "http://localhost:8080/realms/myrealm/authz/protection/permission/ticket?granted=false&returnNames=true" \ -H "Authorization: Bearer $OWNER_TOKEN" | jq
9. Resource Owner: Grant Permission
# Get the ticket ID from listing
TICKET_ID="ticket-uuid"
# Grant permission by updating ticket
curl -s -X PUT \
"http://localhost:8080/realms/myrealm/authz/protection/permission/ticket" \
-H "Authorization: Bearer $OWNER_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"id\": \"$TICKET_ID\",
\"granted\": true
}"
10. Resource Owner: Create UMA Policy
# Create a policy granting access to specific users
curl -s -X POST \
"http://localhost:8080/realms/myrealm/authz/protection/uma-policy/$RESOURCE_ID" \
-H "Authorization: Bearer $OWNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Grant Alice Read Access",
"description": "Allow Alice to read my document",
"scopes": ["read"],
"users": ["alice"]
}'
# Create policy for a group
curl -s -X POST \
"http://localhost:8080/realms/myrealm/authz/protection/uma-policy/$RESOURCE_ID" \
-H "Authorization: Bearer $OWNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Grant Team Access",
"description": "Allow team members to read and write",
"scopes": ["read", "write"],
"groups": ["/my-team"]
}'
# Create policy for a role
curl -s -X POST \
"http://localhost:8080/realms/myrealm/authz/protection/uma-policy/$RESOURCE_ID" \
-H "Authorization: Bearer $OWNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Grant Managers Full Access",
"description": "Allow managers full access",
"scopes": ["read", "write", "delete"],
"roles": ["manager"]
}'
11. Introspect RPT
# Introspect RPT to see permissions
curl -s -X POST \
"http://localhost:8080/realms/myrealm/protocol/openid-connect/token/introspect" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=$RPT" \
-d "client_id=my-resource-server" \
-d "client_secret=secret" | jq
# Response includes permissions:
# {
# "active": true,
# "permissions": [
# {
# "rsid": "resource-id",
# "rsname": "My Document",
# "scopes": ["read"]
# }
# ]
# }
12. Upgrade RPT (Add More Permissions)
# Request additional permissions with existing RPT UPGRADED_RPT=$(curl -s -X POST \ "http://localhost:8080/realms/myrealm/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \ -d "audience=my-resource-server" \ -d "permission=$RESOURCE_ID#write" \ -d "rpt=$RPT" \ -d "client_id=my-client" \ -H "Authorization: Bearer $USER_TOKEN" | jq -r '.access_token')
Complete Example: Document Sharing Application
Scenario
Bob owns a document and wants to share read access with Alice.
#!/bin/bash
KEYCLOAK_URL="http://localhost:8080"
REALM="myrealm"
CLIENT_ID="doc-app"
CLIENT_SECRET="secret"
# Step 1: Bob registers his document
BOB_TOKEN=$(curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token" \
-d "grant_type=password&client_id=$CLIENT_ID&username=bob&password=password" | jq -r '.access_token')
RESOURCE=$(curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/authz/protection/resource_set" \
-H "Authorization: Bearer $BOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Bobs Report Q4",
"type": "document",
"resource_scopes": [{"name": "view"}, {"name": "edit"}],
"ownerManagedAccess": true
}')
RESOURCE_ID=$(echo $RESOURCE | jq -r '._id')
echo "Bob created resource: $RESOURCE_ID"
# Step 2: Bob grants Alice view access
curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/authz/protection/uma-policy/$RESOURCE_ID" \
-H "Authorization: Bearer $BOB_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Alice View Access",
"scopes": ["view"],
"users": ["alice"]
}'
echo "Bob granted Alice view access"
# Step 3: Alice requests access to the document
ALICE_TOKEN=$(curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token" \
-d "grant_type=password&client_id=$CLIENT_ID&username=alice&password=password" | jq -r '.access_token')
RPT=$(curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token" \
-H "Authorization: Bearer $ALICE_TOKEN" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=$CLIENT_ID" \
-d "permission=$RESOURCE_ID#view" | jq -r '.access_token')
echo "Alice obtained RPT for viewing document"
# Step 4: Alice tries to get edit access (will be denied or submitted)
EDIT_RESPONSE=$(curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token" \
-H "Authorization: Bearer $ALICE_TOKEN" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=$CLIENT_ID" \
-d "permission=$RESOURCE_ID#edit" \
-d "submit_request=true")
if echo $EDIT_RESPONSE | jq -e '.error == "request_submitted"' > /dev/null; then
echo "Alice's edit request submitted for Bob's approval"
else
echo "Response: $EDIT_RESPONSE"
fi
# Step 5: Bob views and approves pending requests
PENDING=$(curl -s "$KEYCLOAK_URL/realms/$REALM/authz/protection/permission/ticket?granted=false&returnNames=true" \
-H "Authorization: Bearer $BOB_TOKEN")
TICKET_ID=$(echo $PENDING | jq -r '.[0].id')
curl -s -X PUT "$KEYCLOAK_URL/realms/$REALM/authz/protection/permission/ticket" \
-H "Authorization: Bearer $BOB_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"id\": \"$TICKET_ID\", \"granted\": true}"
echo "Bob approved Alice's edit request"
# Step 6: Alice can now get edit access
EDIT_RPT=$(curl -s -X POST "$KEYCLOAK_URL/realms/$REALM/protocol/openid-connect/token" \
-H "Authorization: Bearer $ALICE_TOKEN" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "audience=$CLIENT_ID" \
-d "permission=$RESOURCE_ID#edit" | jq -r '.access_token')
echo "Alice now has edit access!"
UMA Discovery
Keycloak exposes UMA configuration at the well-known endpoint:
curl -s "http://localhost:8080/realms/myrealm/.well-known/uma2-configuration" | jq
Response:
"issuer": "http://localhost:8080/realms/myrealm",
"authorization_endpoint": "http://localhost:8080/realms/myrealm/protocol/openid-connect/auth",
"token_endpoint": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token",
"introspection_endpoint": "http://localhost:8080/realms/myrealm/protocol/openid-connect/token/introspect",
"resource_registration_endpoint": "http://localhost:8080/realms/myrealm/authz/protection/resource_set",
"permission_endpoint": "http://localhost:8080/realms/myrealm/authz/protection/permission",
"policy_endpoint": "http://localhost:8080/realms/myrealm/authz/protection/uma-policy"
}
Source Code: services/src/main/java/org/keycloak/authorization/config/UmaConfiguration.java
Best Practices
1. Enable Owner-Managed Access
For resources that should be shared by users, always set ownerManagedAccess: true:
"name": "My Resource",
"ownerManagedAccess": true
}
2. Use Specific Scopes
Define granular scopes that map to actual operations:
"resource_scopes": [
{"name": "view"},
{"name": "edit"},
{"name": "delete"},
{"name": "share"}
]
}
3. Implement Proper Error Handling
Handle UMA-specific errors:
| Error | Description | Action |
|---|---|---|
access_denied |
Permission denied | Show access denied message |
request_submitted |
Request sent to owner | Notify user to wait for approval |
invalid_permission |
Invalid resource/scope | Check resource exists |
invalid_ticket |
Ticket expired/invalid | Request new ticket |
4. Token Refresh Strategy
- RPTs have limited lifetime
- Implement token refresh before expiration
- Use
rptparameter to upgrade permissions
5. Resource Cleanup
Delete resources when no longer needed:
curl -X DELETE "$KEYCLOAK_URL/realms/$REALM/authz/protection/resource_set/$RESOURCE_ID" \ -H "Authorization: Bearer $PAT"
Source Code References
| Component | File Path |
|---|---|
| Protection Service | ProtectionService.java |
| Resource Service | ResourceService.java |
| Permission Service | PermissionService.java |
| Permission Ticket Service | PermissionTicketService.java |
| UMA Policy Service | UserManagedPermissionService.java |
| UMA Grant Type | PermissionGrantType.java |
| Authorization Token Service | AuthorizationTokenService.java |
| UMA Policy Provider | UMAPolicyProvider.java |
| Resource Model | Resource.java |
| Permission Ticket Model | PermissionTicket.java |
| UMA Configuration | UmaConfiguration.java |



