← Back to Blog
September 1, 2021

Fine-Grained Authorization with Keycloak UMA: Building Dynamic Resource Access Control

Modern applications increasingly require sophisticated authorization mechanisms that go beyond simple role-based access control. Learn how to architect a system where users dynamically create resources and manage fine-grained permissions through Keycloak's User-Managed Access (UMA) 2.0 implementation.

The Use Case

Consider a multi-tenant SaaS platform where users dynamically create resources—documents, projects, or in our case, car records—and need granular control over who can access them. Our application has the following requirements:

  • A backend server creates car resources dynamically at runtime
  • Each car resource should be individually protected
  • Resource owners need a self-service interface to grant read/write permissions
  • Authorization decisions combine attribute-based (ABAC) and role-based (RBAC) policies
  • The solution must scale to thousands of dynamically created resources

Understanding Keycloak's Authorization Architecture

Before diving into implementation, let's clarify Keycloak's authorization primitives:

Resource Server

The resource server is the application you're protecting—in our case, the backend API that manages car resources. In Keycloak, you register your API as a client with "Authorization Enabled" turned on. This transforms a standard OAuth2/OIDC client into a resource server capable of enforcing fine-grained permissions.

When you enable authorization for a client, Keycloak provisions an authorization configuration for that resource server, including dedicated endpoints for managing resources, scopes, policies, and permissions.

Resources

Resources represent the protected objects in your system. In our scenario, each car is a resource. Resources can be registered programmatically via Keycloak's Protection API, which is critical for dynamic scenarios.

A resource definition includes:

  • Name and URI: Logical identifier and optional URI pattern (e.g., /car/123)
  • Type: Optional categorization (e.g., urn:myapp:resources:car)
  • Scopes: Associated actions that can be performed
  • Owner: The user who owns this resource (crucial for UMA)
  • Attributes: Custom metadata for policy evaluation

Scopes

Scopes define the actions that can be performed on resources. For our use case, we define two scopes:

  • car:read: View car details
  • car:write: Modify car details

Scopes are defined at the resource server level and can be associated with any resource. This allows consistent permission semantics across all car resources.

Policies

Policies are the conditions that must be satisfied for access to be granted. Keycloak supports several policy types:

  • Role Policy (RBAC): Grant access based on realm or client roles
  • User Policy: Grant access to specific users
  • Attribute Policy (ABAC): Evaluate custom attributes from the token or context
  • JavaScript Policy: Custom logic using JavaScript
  • Time Policy: Temporal access restrictions
  • Aggregated Policy: Combine multiple policies with AND/OR logic

For our implementation, we'll combine role-based and attribute-based policies to create sophisticated authorization rules.

Permissions

Permissions bind resources and scopes to policies. They answer the question: "Under what policy conditions can a scope be performed on a resource?"

Keycloak supports two permission types:

  • Resource-based permissions: Apply to specific resources or resource types
  • Scope-based permissions: Apply to scopes across multiple resources

Technical Implementation

Step 1: Configure the Resource Server

First, create a client in Keycloak that represents your backend API:

{
  "clientId": "car-api",
  "protocol": "openid-connect",
  "publicClient": false,
  "authorizationServicesEnabled": true,
  "serviceAccountsEnabled": true,
  "standardFlowEnabled": false,
  "directAccessGrantsEnabled": false
}

The serviceAccountsEnabled flag is crucial—it allows your backend to authenticate with Keycloak to register resources dynamically using client credentials.

Step 2: Define Scopes

In the Keycloak admin console, navigate to your resource server's Authorization settings and define scopes:

  • Name: car:read, Display Name: "Read Car"
  • Name: car:write, Display Name: "Write Car"

Step 3: Dynamic Resource Registration

When your server creates a new car, it must register the resource with Keycloak using the Protection API. Here's the flow:

import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;

import java.util.*;

public class KeycloakResourceManager {

    private final AuthzClient authzClient;

    public KeycloakResourceManager(String serverUrl, String realm,
                                   String clientId, String clientSecret) {
        Map<String, Object> credentials = new HashMap<>();
        credentials.put("secret", clientSecret);

        Configuration configuration = new Configuration(
            serverUrl,
            realm,
            clientId,
            credentials,
            null
        );

        this.authzClient = AuthzClient.create(configuration);
    }

    public String registerResource(String carId, String ownerId,
                                   Map<String, String> carAttributes) {
        ResourceRepresentation resource = new ResourceRepresentation();
        resource.setName("Car " + carId);
        resource.setType("urn:car-api:resources:car");
        resource.setUri("/car/" + carId);
        resource.setOwner(ownerId);
        resource.setOwnerManagedAccess(true); // Enable UMA

        // Define scopes for this resource
        Set<ScopeRepresentation> scopes = new HashSet<>();
        scopes.add(new ScopeRepresentation("car:read"));
        scopes.add(new ScopeRepresentation("car:write"));
        resource.setScopes(scopes);

        // Add custom attributes for ABAC policies
        Map<String, List<String>> attributes = new HashMap<>();
        attributes.put("manufacturer",
            Collections.singletonList(carAttributes.get("manufacturer")));
        attributes.put("category",
            Collections.singletonList(carAttributes.get("category")));
        attributes.put("year",
            Collections.singletonList(String.valueOf(carAttributes.get("year"))));
        resource.setAttributes(attributes);

        // Register with Keycloak
        ResourceRepresentation created = authzClient
            .protection()
            .resource()
            .create(resource);

        return created.getId(); // Keycloak's internal resource ID
    }

    public void deleteResource(String resourceId) {
        authzClient.protection().resource().delete(resourceId);
    }
}

The critical field here is setOwnerManagedAccess(true), which enables UMA for this resource, allowing the owner to manage permissions independently.

Step 4: Create Base Policies

Define policies that will be referenced by permissions. In the Keycloak admin console:

Admin Role Policy (RBAC):

  • Type: Role
  • Name: admin-role-policy
  • Realm roles: admin
  • Logic: Positive

Owner Policy (ABAC):

  • Type: JavaScript
  • Name: resource-owner-policy
  • Code:
var context = $evaluation.getContext();
var identity = context.getIdentity();
var resource = $evaluation.getPermission().getResource();

if (resource.getOwner().equals(identity.getId())) {
    $evaluation.grant();
}

Premium User Read Policy (ABAC):

  • Type: Attribute
  • Name: premium-user-read-policy
  • Condition: User attribute subscription equals premium

Step 5: Define Default Permissions

Create resource-based permissions that apply to all car resources:

Admin Full Access:

  • Name: admin-full-access-permission
  • Resource Type: urn:car-api:resources:car
  • Scopes: car:read, car:write
  • Policies: admin-role-policy
  • Decision Strategy: Unanimous

Owner Full Access:

  • Name: owner-full-access-permission
  • Resource Type: urn:car-api:resources:car
  • Scopes: car:read, car:write
  • Policies: resource-owner-policy
  • Decision Strategy: Unanimous

Premium Read Access:

  • Name: premium-read-permission
  • Resource Type: urn:car-api:resources:car
  • Scopes: car:read
  • Policies: premium-user-read-policy
  • Decision Strategy: Affirmative

Step 6: Enforce Authorization in Your API

When a request arrives at your API, you must request an authorization decision from Keycloak:

import org.keycloak.authorization.client.AuthzClient;
import org.keycloak.authorization.client.resource.AuthorizationResource;
import org.keycloak.representations.idm.authorization.AuthorizationRequest;
import org.keycloak.representations.idm.authorization.AuthorizationResponse;

import javax.ws.rs.*;
import javax.ws.rs.core.Response;

@Path("/car")
public class CarResource {

    private final AuthzClient authzClient;

    public CarResource(AuthzClient authzClient) {
        this.authzClient = authzClient;
    }

    @GET
    @Path("/{carId}")
    @Produces("application/json")
    public Response getCar(@PathParam("carId") String carId,
                          @HeaderParam("Authorization") String authorizationHeader) {

        String accessToken = extractToken(authorizationHeader);

        // Check permission with Keycloak
        if (!checkPermission(accessToken, "/car/" + carId, "car:read")) {
            return Response.status(Response.Status.FORBIDDEN)
                .entity("Access denied")
                .build();
        }

        // Fetch and return car data
        Car car = fetchCarFromDatabase(carId);
        return Response.ok(car).build();
    }

    @PUT
    @Path("/{carId}")
    @Consumes("application/json")
    @Produces("application/json")
    public Response updateCar(@PathParam("carId") String carId,
                             @HeaderParam("Authorization") String authorizationHeader,
                             Car updatedCar) {

        String accessToken = extractToken(authorizationHeader);

        // Check permission with Keycloak
        if (!checkPermission(accessToken, "/car/" + carId, "car:write")) {
            return Response.status(Response.Status.FORBIDDEN)
                .entity("Access denied")
                .build();
        }

        // Update car data
        updateCarInDatabase(carId, updatedCar);
        return Response.ok().entity("{\"status\": \"updated\"}").build();
    }

    private boolean checkPermission(String accessToken, String resourceUri,
                                   String scope) {
        try {
            AuthorizationRequest request = new AuthorizationRequest();
            request.addPermission(resourceUri, scope);

            AuthorizationResponse response = authzClient
                .authorization(accessToken)
                .authorize(request);

            // If we get a response, permission is granted
            return response.getToken() != null;

        } catch (Exception e) {
            // Authorization denied or error occurred
            return false;
        }
    }

    private String extractToken(String authorizationHeader) {
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            return authorizationHeader.substring(7);
        }
        throw new WebApplicationException("Missing or invalid authorization header",
            Response.Status.UNAUTHORIZED);
    }
}

The authorization flow here uses the UMA grant type, which evaluates all policies and permissions associated with the requested resource and scope.

Step 7: Leverage Keycloak's Built-in UMA Account Console

Keycloak provides a built-in Account Console with User-Managed Access capabilities that allows resource owners to manage permissions without any custom development. This is one of the most powerful features of Keycloak's UMA implementation.

Enabling the Account Console:

The Account Console is available by default at:

https://<keycloak-server>/realms/<realm-name>/account

Once users log in to the Account Console, they can navigate to the "Resources" or "My Resources" section, where they'll see:

  1. All resources they own: Every resource registered with ownerManagedAccess: true and their user ID as owner
  2. Resource details: Name, type, URI, and available scopes
  3. Current permissions: List of users who have been granted access and their specific scopes
  4. Share functionality: UI to grant new permissions to other users

How it works:

When a resource owner wants to share access:

  1. They navigate to their resource in the Account Console
  2. Click "Share" or "Add Permission"
  3. Enter the username or email of the user they want to grant access to
  4. Select which scopes to grant (e.g., car:read, car:write)
  5. Submit the permission

Behind the scenes, Keycloak creates a UMA permission ticket that binds the resource, the requester, and the granted scopes.

Deep Linking from Your Application:

You can provide direct links from your application to the relevant sections of the Account Console:

public class AccountConsoleConfig {

    private final String keycloakUrl;
    private final String realm;

    public AccountConsoleConfig(String keycloakUrl, String realm) {
        this.keycloakUrl = keycloakUrl;
        this.realm = realm;
    }

    public String getAccountConsoleUrl() {
        return String.format("%s/realms/%s/account", keycloakUrl, realm);
    }

    public String getMyResourcesUrl() {
        return String.format("%s/realms/%s/account/#/resources", keycloakUrl, realm);
    }

    public String getResourceDetailUrl(String resourceId) {
        return String.format("%s/realms/%s/account/#/resources/%s",
            keycloakUrl, realm, resourceId);
    }
}

Benefits of Using the Built-in Account Console:

  • Zero custom development: No need to build UI for permission management
  • Consistent UX: Users get a familiar, well-tested interface
  • Automatic updates: New Keycloak features appear automatically
  • Security: All permission operations go through Keycloak's validated APIs
  • Audit trail: Keycloak logs all permission changes
  • Multi-language support: Account Console supports internationalization

Decision Strategies and Policy Composition

When multiple policies apply to a permission, Keycloak uses a decision strategy:

  • Unanimous: All policies must grant access
  • Affirmative: At least one policy must grant access
  • Consensus: Majority of policies must grant access

You can create complex authorization logic using aggregated policies. For example:

Permission: "Sensitive Car Write"
├─ Aggregated Policy (AND)
│  ├─ Owner Policy (must be owner)
│  └─ Time Policy (only business hours)
└─ Decision Strategy: Unanimous

Performance Considerations

Dynamic resource registration at scale requires careful consideration:

  1. Caching: Cache authorization decisions at the API layer with short TTLs
  2. Batch Operations: Register resources in batches when possible
  3. Resource Cleanup: Implement lifecycle management to delete resources from Keycloak when cars are deleted
  4. Database Indexing: Ensure your application maintains a mapping between business IDs and Keycloak resource IDs
public class ResourceLifecycleManager {

    private final KeycloakResourceManager resourceManager;
    private final CarRepository carRepository;

    public void createCar(Car car, String ownerId) {
        // 1. Save to database
        carRepository.save(car);

        // 2. Register with Keycloak
        Map<String, String> attributes = new HashMap<>();
        attributes.put("manufacturer", car.getManufacturer());
        attributes.put("category", car.getCategory());
        attributes.put("year", String.valueOf(car.getYear()));

        String keycloakResourceId = resourceManager.registerResource(
            car.getId(),
            ownerId,
            attributes
        );

        // 3. Store mapping
        carRepository.updateKeycloakResourceId(car.getId(), keycloakResourceId);
    }

    public void deleteCar(String carId) {
        // 1. Get Keycloak resource ID
        String keycloakResourceId = carRepository.getKeycloakResourceId(carId);

        // 2. Delete from Keycloak
        if (keycloakResourceId != null) {
            resourceManager.deleteResource(keycloakResourceId);
        }

        // 3. Delete from database
        carRepository.delete(carId);
    }
}

Conclusion

Keycloak's UMA implementation provides enterprise-grade fine-grained authorization for dynamic resource scenarios. By combining resource server configuration, dynamic resource registration, RBAC and ABAC policies, and Keycloak's built-in Account Console, you can build systems where users safely manage their own access control policies without requiring custom UI development.

The architecture we've outlined separates concerns effectively: your application logic focuses on business operations, while Keycloak handles the complex authorization decisions using a standardized, auditable framework. The built-in Account Console eliminates the need for custom permission management UI while still allowing programmatic access when needed.

This approach scales from hundreds to millions of resources while maintaining consistent security policies. For production deployments, consider implementing comprehensive audit logging, monitoring policy evaluation latency, and establishing governance processes around policy creation and modification. The flexibility of Keycloak's authorization services means you can start simple and evolve your authorization model as requirements grow more sophisticated.

Want to Learn More?

Explore Keycloak's comprehensive documentation on authorization services and UMA implementation to dive deeper into fine-grained access control for your applications.

Read Keycloak Documentation