Skip to content

필사 모드: Keycloak Authorization Services — Fine-Grained Access Control with UMA 2.0

English
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

Introduction

A system that can decide authorization with only the question "does this user have the admin role?" hits its limits faster than you might think. The moment a requirement arrives like "can this user **edit this document** — but only if they own it, or they are a manager in the same department, and the request happens during business hours," role-based branching starts to scatter across your entire application codebase.

In 2026 this concern has become even more pressing. In the era of AI agents calling APIs on behalf of users (Keycloak 26.6 experimentally supports the OAuth Client ID Metadata Document for acting as an MCP authorization server), we must be able to centrally evaluate and audit not only "who" but "what is accessing which resource, under which delegation scope." This article dissects the model of Keycloak Authorization Services and the UMA 2.0 flow, walks through practical policy enforcer configuration, performance considerations, and the relationship with external authorization engines such as OPA and OpenFGA.

The Limits of RBAC, and Why ABAC/ReBAC

Let us first map the spectrum of authorization models.

| Model | Decision basis | Example | Limitation |

| --- | --- | --- | --- |

| RBAC | Roles granted to the user | admin can edit all documents | No per-resource distinction, role explosion |

| ABAC | Attributes of subject/resource/environment | Allow if same department and business hours | Hard to author/debug policies |

| ReBAC | Relationship graph between subject and resource | Allow if owner of the document or editor of the folder | Cost of synchronizing relationship data |

The classic failure mode of RBAC is **role explosion**. If you turn "editor of project A" and "viewer of project B" into roles, the role count grows as projects times permissions, and managing roles itself becomes a new authorization problem. Solving it with `if` branches in application code instead scatters policy everywhere, making both auditing and change difficult.

What is needed here is **centralization and externalization of policy**. Keycloak Authorization Services is a framework for centrally evaluating fine-grained, resource-level authorization on top of OAuth 2.0, capable of expressing RBAC as well as ABAC, time-based rules, and a limited ReBAC style.

Architecture of Keycloak Authorization Services

The four building blocks

Keycloak's authorization model consists of four concepts. When you enable Authorization on a client, that client becomes a "resource server," under which you define the following.

+------------------------------------------------------------+

| Resource Server (client) |

| |

| +-----------+ +---------+ |

| | Resource |---->| Scope | "what" (document:123) |

| | (doc,API) | | (view, | "which action" (view, |

| +-----------+ | edit) | edit) |

| ^ +---------+ |

| | ^ |

| +-----+----------------+-----+ |

| | Permission | "binds resources/scopes |

| | (resource-based / | to policies" |

| | scope-based) | |

| +-------------+--------------+ |

| | |

| +-------------v--------------+ |

| | Policy | "who / under which |

| | (role, user, group, time, | conditions" (role, |

| | regex, aggregated ...) | group, time, regex ...) |

| +----------------------------+ |

+------------------------------------------------------------+

- **Resource**: the thing being protected. It can carry URI patterns, a type, and an owner. It may be an individual instance like "document 123" or "the entire document type."

- **Scope**: the actions that can be performed on a resource — verbs like view, edit, delete.

- **Policy**: the unit that defines "who / under which conditions." A policy by itself knows nothing about resources.

- **Permission**: binds a resource (or scope) to policies, declaring "this action on this resource must pass these policies."

This separation matters because of **policy reusability**. Create a "business hours only" time policy once and you can reuse it across dozens of permissions.

Policy types at a glance

| Policy type | Evaluation basis | Use case |

| --- | --- | --- |

| Role | Possession of a realm/client role | Admin-only features |

| User | Specific named users | System account exceptions |

| Group | Group membership (optionally hierarchical) | Department-level access |

| Client | Identity of the requesting client | Internal-service-only APIs |

| Time | Time/day/date-range conditions | Business hours, campaign windows |

| Regex | Regular-expression match on token claims | Email domains, attribute patterns |

| Client Scope | Possession of a client scope | Consent-based access |

| Aggregated | Combination of multiple policies | Composite conditions |

| JavaScript | Arbitrary JS logic (deprecated path) | Legacy custom logic |

A caution is in order about **JavaScript policies**. In the past you could type JS code directly into the Admin Console to create a policy, but for security reasons this is disabled by default and survives only as a deprecated path available through deployed JARs. Considering the trajectory since the removal of the Nashorn engine, new designs should avoid depending on JS policies. If you need complex custom logic: (1) solve it with combinations of existing policy types (aggregated policies), (2) implement a Java-based custom policy via the Policy SPI, or (3) consider pairing with an external authorization engine, discussed below.

The PEP/PDP model

Authorization Services follows the structure of classic XACML terminology.

+----------------+ (1) request +---------------------+

| Client +-------------------->+ Application |

| (browser/app) | | = PEP |

+----------------+ | (Policy Enforcement |

| Point) |

+----------+----------+

| (2) decision request

| (token / ticket)

+----------v----------+

| Keycloak |

| = PDP + PAP |

| (Policy Decision / |

| Administration) |

+----------+----------+

| (3) Permit/Deny

| (RPT issued)

+----------v----------+

| Protected resource |

+---------------------+

- **PEP (Policy Enforcement Point)**: the point on the application side that intercepts requests and enforces authorization decisions. Keycloak's policy enforcer library plays this role.

- **PDP (Policy Decision Point)**: the point that evaluates policies and decides Permit/Deny. The token endpoint of the Keycloak server (UMA grant) is responsible.

- **PAP (Policy Administration Point)**: where policies are managed — the Admin Console and the Protection API.

Dissecting the UMA 2.0 Grant Flow

UMA (User-Managed Access) 2.0 is an extension of OAuth 2.0 with the model that "the resource owner manages access policies for their resources, and clients negotiate authorization through permission tickets." Here is the flow in Keycloak, step by step.

Client Resource Server (PEP) Keycloak (PDP)

| | |

| (1) GET /api/doc/123 | |

| (access token, no RPT) | |

+------------------------->| |

| | (2) request permission ticket|

| +----------------------------->|

| | POST /authz/protection/ |

| | permission |

| |<-----------------------------+

| (3) 401 + WWW-Authenticate: UMA |

| (as_uri, ticket) | |

|<-------------------------+ |

| | |

| (4) POST /token |

| grant_type=uma-ticket, ticket=... |

+-------------------------------------------------------->|

| | (5) policy evaluation |

| (6) RPT (token with embedded permissions) |

|<--------------------------------------------------------+

| | |

| (7) GET /api/doc/123 (RPT) |

+------------------------->| (8) verify RPT, respond |

|<-------------------------+ |

Permission tickets and the RPT

Two token concepts appear here.

- **Permission ticket**: a one-time ticket the resource server obtains by registering with Keycloak the fact that "this request requires the view permission on document:123." The client takes this ticket to the token endpoint to request authorization.

- **RPT (Requesting Party Token)**: an **access token with embedded permission claims**, issued to a client that passed policy evaluation. Which scopes on which resources were granted is encoded inside the token.

In actual HTTP requests it looks like this.

POST /realms/myrealm/protocol/openid-connect/token HTTP/1.1

Host: sso.example.com

Authorization: Bearer ACCESS_TOKEN

Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:uma-ticket

&ticket=PERMISSION_TICKET_VALUE

&submit_request=false

You can also skip the ticket and specify the resource server's client_id as the audience, asking "evaluate all the permissions I hold." This is a practical shortcut that skips the full UMA round trip and is used frequently.

POST /realms/myrealm/protocol/openid-connect/token HTTP/1.1

Host: sso.example.com

Authorization: Bearer ACCESS_TOKEN

Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:uma-ticket

&audience=document-service

&permission=document-resource#view

&response_mode=decision

With `response_mode=decision`, you receive a simple allow/deny JSON instead of an RPT — handy for single-shot decision evaluation when you do not need an RPT.

{

"result": true

}

The permission claim of an issued RPT has the following structure.

{

"authorization": {

"permissions": [

{

"rsid": "8f4d2e1a-resource-uuid",

"rsname": "document-123",

"scopes": ["view", "comment"]

}

]

},

"aud": "document-service",

"exp": 1781234567

}

The Protection API and dynamic resource registration

The other pillar of UMA is the Protection API. A resource server can dynamically register and manage resources using a service account token (PAT, Protection API Token). The typical pattern: "when a user creates a document, register that document as a resource together with its owner."

Dynamically register a resource when a document is created

curl -X POST "https://sso.example.com/realms/myrealm/authz/protection/resource_set" \

-H "Authorization: Bearer PAT_TOKEN" \

-H "Content-Type: application/json" \

-d '{

"name": "document-123",

"type": "urn:document-service:resources:document",

"owner": "jdoe",

"ownerManagedAccess": true,

"resource_scopes": ["view", "edit", "delete", "share"],

"uris": ["/api/documents/123"]

}'

With `ownerManagedAccess` enabled, the resource owner can share access with other users or approve/deny access requests directly from the Account Console. User-driven sharing scenarios — "share my document with a colleague" — are the original design purpose of UMA.

Policy Enforcer in Practice (Java)

Let us bring the theory down to code. The `keycloak-policy-enforcer` library provided by Keycloak is the standard way to embed a PEP into a Java application (the old Keycloak adapters are deprecated; the combination of Spring Security plus the policy enforcer is the currently recommended path).

<!-- pom.xml -->

The enforcer configuration file:

{

"realm": "myrealm",

"auth-server-url": "https://sso.example.com",

"resource": "document-service",

"credentials": {

"secret": "CLIENT_SECRET_FROM_VAULT"

},

"http-method-as-scope": true,

"lazy-load-paths": true,

"enforcement-mode": "ENFORCING",

"paths": [

{

"path": "/api/documents/*",

"claim-information-point": {

"claims": {

"request.ip": "{request.remoteAddr}"

}

}

},

{

"path": "/api/health",

"enforcement-mode": "DISABLED"

}

]

}

Wire the enforcer into the Spring Security filter chain:

@Configuration

@EnableWebSecurity

public class SecurityConfig {

@Bean

public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

http

.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))

.authorizeHttpRequests(authz -> authz.anyRequest().authenticated())

.addFilterAfter(createPolicyEnforcerFilter(), BearerTokenAuthenticationFilter.class);

return http.build();

}

private ServletPolicyEnforcerFilter createPolicyEnforcerFilter() {

return new ServletPolicyEnforcerFilter(request -> {

try (InputStream is = getClass().getResourceAsStream("/policy-enforcer.json")) {

return JsonSerialization.readValue(is, PolicyEnforcerConfig.class);

} catch (IOException e) {

throw new UncheckedIOException(e);

}

});

}

}

Configuration points worth highlighting:

- `http-method-as-scope`: maps HTTP methods (GET, POST, DELETE) to scopes. For REST APIs this simplifies resource definitions considerably.

- `lazy-load-paths`: instead of fetching all resources at startup, resolves resources per path at request time. Essential once you have thousands of resources.

- `enforcement-mode`: ENFORCING (deny unmapped paths), PERMISSIVE (allow unmapped paths), or DISABLED — also configurable per path. A safe gradual rollout starts with PERMISSIVE while you build up coverage.

- `claim-information-point` (CIP): extracts request context (IP, headers, body) as claims and passes them into policy evaluation — the input for ABAC-style policies.

Decision Strategy — Interpreting Policy Conflicts

When multiple policies are attached to one permission, the decision strategy determines how results are combined.

| Strategy | Meaning | Use case |

| --- | --- | --- |

| Unanimous (default) | Permit only if all policies permit | Security-first, recommended default |

| Affirmative | Permit if any single policy permits | "Admin OR owner" |

| Consensus | Permit if permits outnumber denies | Rare, voting-style scenarios |

A common trap is **using Unanimous where Affirmative is needed**. Bundling "owner policy + admin role policy" under Unanimous means only users who are simultaneously owner AND admin pass. If you mean OR, change the permission's decision strategy to Affirmative, or restructure within an aggregated policy that specifies the strategy.

Note also that there is a separate resource-server-level decision strategy (how results are combined when multiple permissions apply to the same resource). Confusing the permission-level and resource-server-level strategies makes debugging painful, so build the habit of simulating in the Evaluate tab of the Admin Console. The Evaluate tab shows step by step which policy produced which result for a given user/resource/scope combination — the single most useful debugging tool in this feature set.

Performance Considerations

Fine-grained authorization is not free. Review the following at design time.

- **Evaluation round-trip cost**: in ENFORCING mode the PEP sends an evaluation request to Keycloak on cache misses. This can add several to tens of milliseconds per request, so tune the enforcer's path/decision caches and leverage RPT reuse (let clients carry the RPT).

- **Resource count explosion**: an instance-level model that registers a resource per document can lead to millions of resources. Keycloak stores resources/policies in an RDBMS, so at that scale consider a hybrid model — type-level resources plus CIP for instance decisions (comparing owner claims) — or an external ReBAC engine.

- **Token size**: an RPT carrying hundreds of permissions can grow to tens of kilobytes and hit header limits. Request only the needed resources via the `permission` parameter, or use decision mode.

- **DB load**: policy evaluation involves Keycloak DB queries. Factor into capacity planning that authorization traffic is typically much higher than login traffic.

Empirical guidelines

------------------------------------------------------

Resources < 10k, eval QPS < hundreds → Keycloak authz alone is enough

Resources 100k+, relationship-driven → consider external ReBAC (OpenFGA)

Policy is code/data-centric (deploy pipeline rules etc.) → consider OPA

Comparison with External Authorization Engines — OPA, OpenFGA

In the 2026 authorization ecosystem, Keycloak Authorization Services is not the only option. Let us compare it with the two leading external engines.

| Aspect | Keycloak Authz | OPA (Rego) | OpenFGA (Zanzibar lineage) |

| --- | --- | --- | --- |

| Model | resource/scope/policy | General-purpose policy engine (strong ABAC) | Relationship tuple graph (ReBAC) |

| Policy language | Admin UI + policy types | Rego (declarative language) | DSL (relationship model definition) |

| Data location | Keycloak DB | Injected as input (sidecar/bundles) | Own tuple store |

| Strengths | IdP integration, UMA, user-driven sharing | Universal policy across infrastructure | Large-scale relationship queries (listObjects) |

| Weaknesses | Very large resource sets, relationship graphs | Data synchronization is your problem | No IdP features, separate operations |

| Standards | UMA 2.0, OAuth | De facto standard (CNCF) | Based on the Zanzibar paper, engaged in OpenID AuthZEN |

The key insight is that **these are layers, not competitors**. Keycloak is the source of truth for "who was authenticated and what token they hold," while OPA/OpenFGA can act as PDPs that take that token as one of their inputs and make more complex decisions.

Patterns for using them together

Combination patterns proven in practice:

Pattern A: Keycloak (authn + coarse) + OPA (fine-grained, infra policy)

+--------+ JWT +-------------+ input(JWT claims, +-----+

| Client +-------->+ API Gateway +-------------------->+ OPA |

+--------+ | / Service | resource attrs) +-----+

+-------------+<--------------------+

allow / deny

Pattern B: Keycloak (authn) + OpenFGA (relationship-based authz)

+--------+ JWT +----------+ check(user, relation, +---------+

| Client +-------->+ Service +----------------------->+ OpenFGA |

+--------+ +----------+ object) +---------+

| ^

| sync relationship tuples |

| on writes |

+-----------------------------------+

- **Pattern A (Keycloak + OPA)**: claims from the Keycloak-issued JWT (roles, groups, department attributes) become inputs to OPA policies. OPA excels at gateway/service-mesh-level policy (this path is internal-only, this API requires a specific scope), while Keycloak is responsible for user identity attributes. The connective tissue is using Keycloak protocol mappers to put the attributes needed for policy evaluation into the token.

- **Pattern B (Keycloak + OpenFGA)**: OpenFGA, heir to the Google Zanzibar paper, is strong at models where relationships propagate ("document → folder → team") and at "list all documents this user can see" (listObjects) queries. Keycloak provides authentication and user identifiers, and domain services write relationship tuples to OpenFGA when resources are created or shared.

- **Hybrid**: the general solution for large systems has become a two-tier structure — coarse-grained checks (may this API be called at all) baked into tokens as Keycloak scopes/roles and filtered quickly at the gateway, with fine-grained checks (may this object be accessed) evaluated inside services via OpenFGA/OPA.

Where Keycloak Authorization Services shines brightest is **UMA-based user-driven resource sharing** (Account Console integration) and the **operational simplicity of unifying IdP and policy management**. Conversely, if relationship queries over millions of objects are your core need, you are better off introducing a ReBAC engine as a separate layer from the start.

Operational Best Practices

- **Start PERMISSIVE, then go ENFORCING**: when introducing this into an existing service, keep enforcement mode PERMISSIVE, collect would-be-denied logs, validate policy coverage, then switch.

- **Treat the Evaluate tab like CI**: build a regression test that automatically verifies representative scenarios (owner, other department, anonymous, outside business hours) through the Evaluate API whenever policies change. This dramatically reduces policy incidents.

- **Policy naming conventions**: without a consistent scheme like policy-target-condition (e.g., policy-document-owner-only), you will get lost among dozens of policies.

- **Version-control via export**: a client's authorization settings can be exported/imported as JSON. Commit policies to Git and build a pipeline that promotes them across environments.

- **Eliminate JS policy dependencies**: JS policies are debt on the upgrade path. Plan replacements with aggregated/regex/CIP combinations or SPI implementations.

- **Audience validation**: services receiving RPTs must verify the aud claim names them. This is the basic defense against replaying an RPT meant for another service.

Troubleshooting Notes

| Symptom | Candidate cause | Response |

| --- | --- | --- |

| Every request returns 403 | ENFORCING + unmapped paths | Path config; isolate with PERMISSIVE |

| Owner is denied | Decision strategy is Unanimous | Switch to Affirmative or restructure aggregation |

| RPT has empty permissions | Missing audience, resource not matched | Audience parameter, resource URI patterns |

| Evaluation is slow | No lazy-load, too many resources | lazy-load-paths, redesign to type-level resources |

| Token too large | Full permission set in RPT | Narrow with permission parameter, decision mode |

| Policy changes not taking effect | Enforcer cache | Check cache TTLs, restart/invalidate |

Closing

Keycloak Authorization Services is a deeper framework than it first appears — an implementation, on top of the OAuth/UMA standards, of the goal "pull authorization logic out of code and manage it declaratively in one place." Once you understand the four-way model of resource/scope/policy/permission and the decision strategies, requirements that were impossible to express with RBAC can be solved elegantly, and the UMA 2.0 permission ticket flow provides the unique value of user-driven sharing.

At the same time, the limits are clear. Relationship queries over millions of resources belong to Zanzibar-lineage engines, and universal policy across infrastructure belongs to OPA. The realistic answer in 2026 is "Keycloak for authentication and coarse-grained authorization, with ReBAC/policy engines layered on where needed" — and producing the trustworthy token that feeds every one of those layers remains Keycloak's unchanging role. In the next article we cover Keycloak observability — metrics, audit logs, and event-driven monitoring.

References

- [Keycloak Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/index.html)

- [Keycloak Documentation](https://www.keycloak.org/documentation)

- [Keycloak 26.6.0 Release Notes](https://www.keycloak.org/docs/latest/release_notes/index.html)

- [UMA 2.0 Grant for OAuth 2.0 Authorization](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-grant-2.0.html)

- [Federated Authorization for UMA 2.0](https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html)

- [RFC 6749 — The OAuth 2.0 Authorization Framework](https://datatracker.ietf.org/doc/html/rfc6749)

- [RFC 9700 — Best Current Practice for OAuth 2.0 Security](https://datatracker.ietf.org/doc/html/rfc9700)

- [RFC 8693 — OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693)

- [Open Policy Agent Documentation](https://www.openpolicyagent.org/docs/latest/)

- [OpenFGA Documentation](https://openfga.dev/docs)

- [Google Zanzibar Paper](https://research.google/pubs/pub48190/)

- [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/)

현재 단락 (1/278)

A system that can decide authorization with only the question "does this user have the admin role?" ...

작성 글자: 0원문 글자: 19,803작성 단락: 0/278