Implementing fine-grained authorization with Keycloak's Authorization Services: resources and scopes, policies based on role, group, user, client, time, and JavaScript, plus the permission evaluation flow that produces an RPT.

In episode 24 you secured the entrance: brute force protection, security headers, and token security. Episode 25 goes deeper — from who may sign in to what may be done after signing in. Authentication answers identity; authorization answers permission. Keycloak provides the Authorization Services module for managing access at the resource level, not just per role.
Authorization Services is enabled per client via the Authorization tab on the client page. Once enabled, Keycloak becomes a policy decision point that evaluates every access request based on the resources, scopes, policies, and permissions you define.
This approach is resource-based: decisions are made per resource, considering the request context — who asks, from which IP, at what time, and what data is involved. This differs from classic RBAC, which only looks at the user's role label. With context-based decisions, two users with the same role can get different decisions because their time or location differs.
Several core concepts you'll use constantly:
| Concept | Role |
|---|---|
| Resource | the protected object, e.g. a document, API endpoint, or file |
| Scope | the action that can be performed on a resource: view, edit, delete |
| Policy | a logic rule: a specific role, a specific user, a specific time |
| Permission | the binder between a resource and a policy |
| RPT | the token resulting from evaluation, carrying grants for resources |
Keycloak implements UMA 2.0 as its foundation — a standard that lets resource owners govern who may access what, with a structured permission ticket flow.
The first step is modeling what's protected. In the Authorization → Resources tab, you define resources with a name, type, URI, and attributes. Scopes describe the actions that can be performed:
{
"name": "Transaksi",
"type": "urn:bank:transaction",
"uris": ["/transactions/*"],
"scopes": ["view", "edit", "delete"],
"attributes": {
"region": "jakarta"
}
}attributes are resource attributes — metadata a policy can use for context-based decisions. For example the region attribute can be the basis for a rule like "can only be edited from the same region". uris connect the resource to the protected API endpoints, so enforcement can be tied directly to request routes. Scopes can also be arranged as separate scope permissions so their usage is controlled per scope.
A policy is the decision logic. Keycloak provides many types that can be combined:
| Policy Type | Logic |
|---|---|
| Role-based | allows holders of a specific role |
| User-based | allows specific users |
| Group-based | allows members of a specific group |
| Client-based | allows specific clients |
| Time-based | only valid within a certain time range |
| JavaScript | logic freely written in JavaScript |
| Aggregated | combines several policies with AND or OR rules |
Role-based policy is the most common: "role teller may view the Transaksi resource". Time-based is useful for working hours: "transfers only allowed from 08.00 to 17.00". JavaScript policy gives full flexibility — for example comparing a request attribute with a resource attribute directly. Aggregated policy combines them all: role teller AND working hours AND region jakarta. Client-based is useful in service-to-service scenarios, where access is determined by the calling application's identity, not just the user.
A permission is the binder between a resource (or scope) and one or more policies. Keycloak recognizes two forms:
{
"name": "Teller boleh view transaksi",
"type": "resource",
"resources": ["Transaksi"],
"scopes": ["view"],
"policies": ["Hanya role teller", "Hanya jam kerja"]
}Evaluation is AND across the policies within one permission: both policies above must pass before access is granted. This is the most common form of context-based decisions — the user's role is correct, but outside working hours it's still denied.
When an application requests access, the sequence is:
The most common way to obtain an RPT is through the token endpoint with UMA ticket grant:
curl -X POST "https://sso.example.com/realms/bank/protocol/openid-connect/token" \
-H "Authorization: Bearer <access_token>" \
-d "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
-d "client_id=bank-app" \
-d "audience=bank-app" \
--data-urlencode "permission=Transaksi#view"The permission parameter is filled with a resource and scope combination separated by a hash, for example the resource Transaksi with the scope view. urn:ietf:params:oauth:grant-type:uma-ticket is a special grant type meaning "give me an RPT for this permission". Keycloak also provides an authorization request endpoint (/authz/authorize) and an entitlements endpoint for older patterns — retrieving all of a user's entitlements against a resource server at once — while modern flows run through the token endpoint.
Architecturally, decision and enforcement are separated:
To protect an API, the PEP is installed at the gateway or inside the service. In microservices architectures, the recommended pattern: one central PDP, while every service installs a thin PEP — decisions stay centralized, enforcement spreads across many points.
Important
Don't place decision logic inside the application. Rules must live in Keycloak as the PDP so audit and policy changes are centralized. The PEP only asks for a decision and enforces it — it doesn't decide.
Episode 25 introduced fine-grained authorization: Authorization Services per client with the resource, scope, policy, and permission concepts; policies based on role, group, user, client, time, JavaScript, and aggregated; and the evaluation flow from authorization request to permission ticket and RPT. You also understood the PEP and PDP separation, plus implementation patterns for APIs and microservices.
Key takeaways:
In the next episode (episode 26), you'll learn to manage clients at scale: client registration & dynamic clients — registering applications automatically via the API, using initial access tokens, and choosing the right client authentication method.