Keycloak UMA 2.0 Implementation

By | February 2, 2026

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

Mermaid Diagram 1 - string

Architecture

Keycloak Authorization Services Components

Keycloak implements the full UMA 2.0 specification through its Authorization Services:

Mermaid Diagram 2

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

Mermaid Diagram 3

Alternative: Permission Request Flow (submit_request)

When a requesting party doesn’t have permission, they can submit a request to the resource owner:

Mermaid Diagram 4

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="">&gt; 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="">&gt; 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="">&gt; claims = new HashMap&lt;&gt;();
    for (PermissionRequest permissionRequest : request) {
        Map<string, list<string="">&gt; 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 &amp;&amp;
                        resource.isOwnerManagedAccess() &amp;&amp;
                        !resource.getOwner().equals(identity.getId()) &amp;&amp;
                        !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&amp;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&amp;client_id=$CLIENT_ID&amp;username=bob&amp;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&amp;client_id=$CLIENT_ID&amp;username=alice&amp;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"' &gt; /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&amp;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 rpt parameter 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

Leave a Reply