Estimated read: about 13 minutes
AFFIRMATIVE, UNANIMOUS, or CONSENSUS. Enforcement mode decides what happens when no policy matches.In our Keycloak 101 post we built a fictional B2B SaaS called InvoiceFlow and gave its users roles: admin, approver, viewer. Roles travel as claims inside the access token, and the application reads them:
if (token.roles.includes('approver')) {
// show the Approve button
}
That works beautifully until the rules stop being about who you are and start being about what you are touching and under what circumstances. The founder comes back with the real requirements:
None of these are answerable from a list of role names. The decision now turns on resource attributes (the invoice amount, its department), on runtime context (time, region), and on relationships (owner, requester). This is exactly the wall RBAC hits, and it is the gap Keycloak Authorization Services is built to fill.
Authorization Services moves the decision off the client and into Keycloak. Instead of every service re-implementing "can this person do this," they ask one engine and trust its verdict.
The rest of this post is about how that engine is wired: the objects you configure, how they relate, and the exact path a request takes from "I'd like to approve invoice 123" to a signed permit.
Authorization Services has exactly five nouns. Most confusion about Keycloak authorization is really confusion about these five and how they connect.
A Resource Server is not a new kind of object you create from scratch. In Keycloak's own words, it is "basically an existing client application in Keycloak that will also act as a resource server." You take a confidential client, say InvoiceFlow's backend API, flip on Authorization, and it becomes the container that owns everything else.
The resource server carries two settings that decide the outcome of every evaluation it performs:
policyEnforcementMode decides what to do when no policy matches a request (ENFORCING, PERMISSIVE, or DISABLED).decisionStrategy decides how to combine multiple permissions that touch the same resource or scope. The Admin Console offers AFFIRMATIVE or UNANIMOUS here at the resource-server level; CONSENSUS is available on individual policies and permissions (and can be set on the resource server over REST).Straight from the DecisionStrategy enum's own documentation:
| Strategy | Grants access when |
|---|---|
AFFIRMATIVE |
at least one policy says permit |
UNANIMOUS (default) |
every policy says permit |
CONSENSUS |
more permits than denies; a tie is a deny |
It also has allowRemoteResourceManagement, which controls whether the application may register its own resources at runtime through the Protection API (more on that later). Internally the resource server is tied to its backing client by clientId.
A Resource is the thing you are protecting. In InvoiceFlow that might be a specific invoice (invoice-123) or a whole category (invoice). The model has more to it than a name and a URI:
| Field | Meaning |
|---|---|
name |
Unique name within the resource server |
displayName |
Human-friendly label |
type |
A classifier shared by many instances, e.g. urn:invoiceflow:resources:invoice |
uris |
One or more URIs the enforcer matches requests against |
scopes |
The actions available on this resource |
owner |
Who owns it: the resource server, or an end user |
ownerManagedAccess |
Whether the owner can share it (UMA) |
attributes |
Arbitrary key/value metadata available to policies |
Two distinctions matter. First, a resource can be an instance (invoice-123, owned by Alice) or a type (invoice, owned by the application). Type lets you write one policy that covers every invoice. Second, there is a version nuance worth knowing: up to Keycloak 26.4, enabling Authorization auto-created a Default Resource (URI /*), a Default Policy that always granted, and a Default Permission, so a fresh resource server started in a deliberately permissive state you were expected to tighten. Keycloak 26.5 removed that auto-creation. On current Keycloak a freshly enabled resource server starts empty, which under the default ENFORCING mode means it denies everything until you add a policy.
A resource belongs to exactly one resource server. That ownership is the "relationship between Resource Servers and Resources" at the heart of the model: the resource server is the policy boundary, and resources are the protected assets inside it.
A Scope is an action or aspect of a resource: read, approve, delete, print. Keycloak describes it as something "usually associated with one or more resources in order to define the actions that can be performed." Scopes are how you get verbs into your authorization model instead of protecting resources as all-or-nothing blobs.
This is also the single biggest source of confusion in Keycloak, so it gets its own section next.
A Policy is a reusable condition. It answers "is this true?" without knowing or caring which resource it will be attached to. A policy has a type (the kind of condition), a logic (positive or negated), and a bag of type-specific config. Keycloak ships a dozen policy types:
role, user, client, client-scope, group, time, regex, aggregate, js, plus the resource, scope, and uma permission types that are themselves modelled as policies under the hood.
Because policies are decoupled from resources, you write "user is in the managers group" once and reuse it everywhere. Policies can also reference other policies (associatedPolicies), which is how aggregate policies and permissions compose smaller rules into bigger ones.
A Permission is the glue. It binds one or more policies to the resources and/or scopes they protect. There are two flavours:
invoice resources, these policies must pass."approve scope on invoice, these policies must pass." This one is finer-grained, because it targets the verb.So the full chain reads like this. A Resource Server owns Resources; each resource exposes Scopes; Permissions attach Policies to those resources and scopes; and at request time the engine evaluates the relevant permissions and returns permit or deny.
When a client logs in it requests OAuth scopes such as openid profile email. Those are part of the OpenID Connect handshake, they can trigger a consent screen, and they decide which claims land in the access token. They are about the token.
Authorization scopes (read, approve) look identical on the page and are completely different in nature. They are never granted by consent, and they never appear because the user asked for them. They show up only after the policy engine evaluates a permission and decides the subject is allowed that action on that resource.
OAuth scopes shape the token at login. Authorization scopes are what a policy decision produces. Same word, unrelated jobs.
Mixing them up causes real bugs. A team adds approve as a client scope, sees it in the token, and assumes the user is authorized, when in fact no policy was ever evaluated.
Access-control systems are usually described with four roles. Keycloak maps onto all four, which is a handy way to keep the moving parts straight.
The takeaway is that, within this model, the application does not make the authorization decision itself. It enforces one made centrally by Keycloak, from the information Keycloak gathers at evaluation time. That does not mean the application decides nothing. Most services still run their own input validation, business rules, and ownership checks; what moves to Keycloak is specifically the "can this subject perform this action on this resource" verdict, which the application then trusts and enforces.
Most write-ups stop at the diagram. The useful part is the evaluator.
The logic lives in DefaultPolicyEvaluator; here is a simplified excerpt of that control flow:
public void evaluate(ResourcePermission permission, AuthorizationProvider authz,
EvaluationContext context, Decision decision, ...) {
ResourceServer resourceServer = permission.getResourceServer();
PolicyEnforcementMode enforcementMode = resourceServer.getPolicyEnforcementMode();
// 1. Authorization switched off for this resource server -> always permit.
if (PolicyEnforcementMode.DISABLED.equals(enforcementMode)) {
grantAndComplete(permission, authz, context, decision);
return;
}
// 2. Permission already granted upstream (e.g. a granted UMA ticket) -> permit.
if (permission.isGranted()) {
grantAndComplete(permission, authz, context, decision);
return;
}
AtomicBoolean verified = new AtomicBoolean();
Consumer<Policy> policyConsumer = createPolicyEvaluator(permission, authz, context, decision, verified, ...);
Resource resource = permission.getResource();
// 3. Gather every policy that could apply, and run each one.
if (resource != null) {
evaluateResourcePolicies(permission, authz, policyConsumer); // bound to this resource
evaluateResourceTypePolicies(permission, authz, policyConsumer); // bound to its type
}
evaluateScopePolicies(permission, authz, policyConsumer); // bound to the requested scopes
// 4. At least one policy ran -> let the collected decisions stand.
if (verified.get()) {
decision.onComplete(permission);
return;
}
// 5. No policy matched. PERMISSIVE allows; ENFORCING (the default) falls through to deny.
if (PolicyEnforcementMode.PERMISSIVE.equals(enforcementMode)) {
grantAndComplete(permission, authz, context, decision);
}
}
Walking through it:
DISABLED, evaluation is skipped and access is granted. This is for switching the whole feature off, not for production.invoice type covers every invoice), and those attached to the scopes being requested. Each policy is handed to its provider, which sets a PERMIT or DENY effect.ENFORCING, the default, the request is denied. Under PERMISSIVE it is allowed. This single switch is the most common foot-gun in Keycloak authorization, so it is worth restating:
ENFORCINGdenies a resource that has no policy.PERMISSIVEallows it. If you turn on Authorization and suddenly everything is 403, you are inENFORCINGwith no policies yet, and that is the system working as designed.
A resource can be covered by several permissions, and a permission can chain several policies. Keycloak collapses all those individual PERMIT/DENY effects into one verdict using the decision strategy. Within a single permission, the fold over its associated policies lives in AbstractDecisionCollector.isGranted():
switch (decisionStrategy) {
case AFFIRMATIVE: // at least one PERMIT wins
for (var d : policyResult.getAssociatedPolicies())
if (Effect.PERMIT.equals(d.getEffect())) return true;
return false;
case CONSENSUS: // permits must outnumber denies; a tie denies
int grant = 0, deny = policy.getAssociatedPolicies().size();
for (var d : policyResult.getAssociatedPolicies())
if (d.getEffect() == Effect.PERMIT) { grant++; deny--; }
return grant > deny;
default: // UNANIMOUS (default): any DENY loses
for (var d : policyResult.getAssociatedPolicies())
if (Effect.DENY.equals(d.getEffect())) return false;
return true;
}
Those are the three strategies from the comparison table back in the Resource Server section, here as real control flow: AFFIRMATIVE needs a single permit, UNANIMOUS fails on any deny, and CONSENSUS counts the votes, with a tie denying.
The strategy exists at two levels: on the resource server, where it resolves conflicts between permissions, and on an individual policy or permission, where it resolves its associated policies. The cross-permission fold at the resource-server level happens a layer up, in DecisionPermissionCollector, which accumulates the granted and denied scopes and consults the resource server's own strategy. A new resource server defaults to UNANIMOUS, which is the safe choice, since every applicable rule has to agree before access is granted.
logic twistOne more subtlety. Every policy has a logic of POSITIVE or NEGATIVE. NEGATIVE inverts the policy's own outcome, which is how you express "everyone except contractors." You can see it in DefaultEvaluation.grant():
public void grant() {
if (policy != null && Logic.NEGATIVE.equals(policy.getLogic())) {
setEffect(Effect.DENY); // a "granted" negative policy actually denies
} else {
setEffect(Effect.PERMIT);
}
}
So a policy that "matches" can still contribute a DENY if its logic is negated. Small flag, large blast radius. It is worth a comment in your policy descriptions whenever you reach for it.
Configuration is half the story. The other half is what happens on the wire when a real request arrives. One common runtime pattern uses the User-Managed Access (UMA) 2.0 grant, with the token endpoint playing the PDP. This is the flow you get when the enforcer is configured to challenge with permission tickets; it is not the only way enforcement plays out, as the end of this section spells out.
POST /invoices/123/approve.invoice-123#approve in the token and replies 401 Unauthorized with a permission ticket, an opaque handle that names the resource and scope being requested.curl -X POST \
"https://auth.example.com/realms/{realm}/protocol/openid-connect/token" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
--data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
--data "audience=invoiceflow-api" \
--data "permission=invoice-123#approve"
The call above uses Keycloak's ticketless variant: rather than echoing the 401 ticket back, the client names the resource server with audience and the resource and scope with permission (the resource part may be the resource id, which is the default, or its name as shown here, resolved as a fallback). The strict UMA flow instead replays the challenge with --data "ticket=${PERMISSION_TICKET}". Both reach the same evaluation engine and return the same kind of token.
authorization claim:{
"aud": "invoiceflow-api",
"exp": 1716300000,
"authorization": {
"permissions": [
{
"rsid": "a1b2c3d4-e5f6-7890",
"rsname": "invoice-123",
"scopes": ["approve", "read"]
}
]
}
}
invoice-123#approve in the token's permissions and lets it through.DecisionPermissionCollector builds each granted permission into the rsid / rsname / scopes entries you see above. There is also a lighter-weight entitlement mode: send the UMA grant with no specific permission, and Keycloak returns every permission the subject currently has on that resource server. That is handy for priming a UI.
Two related modes are worth knowing. If you only need a yes or no, add response_mode=decision and Keycloak skips minting a full RPT, returning a bare { "result": true } instead. That is cheaper on the hot paths discussed later. And when a resource server receives an RPT, it can validate it server-side through token introspection with token_type_hint=requesting_party_token, which echoes back the embedded permissions.
The ticket-and-RPT dance above is one configurable enforcer behavior, not a universal law. Depending on how the policy enforcer is set up, a denied request can instead come back as a plain 403 Forbidden (the token endpoint itself answers 403 with access_denied when a request maps to no permission), or redirect the caller when on-deny-redirect-to is configured. Under PERMISSIVE enforcement mode unmapped requests pass straight through, and under DISABLED enforcement is skipped entirely. An enforcer can also skip acquisition altogether and enforce the permissions already carried in an RPT, decoding and validating the token locally rather than calling back for a new one. Treat the sequence above as the canonical UMA case, then pick the enforcer behavior that fits your service.
You can drive all of this from the Admin Console (Clients → your client → Authorization) or via REST. The shape is always the same: enable authorization, define resources and scopes, write policies, bind them with permissions.
For automation, the Protection API lets a resource server manage its own resources at runtime (when allowRemoteResourceManagement is on). Registering a resource looks like this:
curl -X POST \
"https://auth.example.com/realms/{realm}/authz/protection/resource_set" \
-H "Authorization: Bearer ${PAT}" \
-H "Content-Type: application/json" \
-d '{
"name": "invoice-123",
"type": "urn:invoiceflow:resources:invoice",
"uris": ["/invoices/123"],
"scopes": [{ "name": "read" }, { "name": "approve" }],
"ownerManagedAccess": false
}'
The ${PAT} is a protection API token, an access token for the resource server's own client carrying the uma_protection scope. The Protection API also exposes permission and policy endpoints, which is what powers owner-managed sharing ("let this auditor see invoice 123") without an administrator editing roles.
Wherever you create them, the two resource-server switches, policyEnforcementMode and decisionStrategy, sit on the Authorization Settings tab and quietly govern everything underneath.
Take one of the rules from the top of this post: an approver can approve an invoice only if they are a manager, and only during business hours. That is two policies bound to one scope-based permission. Against the resource server's admin endpoints (/admin/realms/{realm}/clients/{id}/authz/resource-server):
# A role policy: the subject must hold the manager role
POST .../policy/role
{ "name": "Managers only", "roles": [{ "id": "manager", "required": true }] }
# A time policy: only between 09:00 and 18:00
POST .../policy/time
{ "name": "Business hours", "hour": "9", "hourEnd": "18" }
# A scope-based permission binding both to invoice#approve, UNANIMOUS
POST .../permission/scope
{
"name": "Approve invoices",
"resources": ["invoice"],
"scopes": ["approve"],
"policies": ["<managers-policy-id>", "<business-hours-policy-id>"],
"decisionStrategy": "UNANIMOUS"
}
UNANIMOUS on the permission is what makes it manager and business hours; switch it to AFFIRMATIVE and either condition alone would grant. The other two rules from the intro, the €50,000 threshold and the department match, depend on the invoice's own data rather than on identity. Those need a policy that can read attributes: either resource attributes on the invoice resource, or claims the enforcer pushes into the evaluation context through its Claim Information Point. Keycloak has no built-in "claim-based" policy type for this; such attributes and claims are evaluated by a JAR-deployed JS policy or a custom policy provider. Here is the hard truth: writing the policy is the easy half. The real work is feeding it trustworthy attributes. The invoice amount and department need to reach the decision somehow either as resource attributes synced with your domain data or as claims a gateway injects at runtime. Keycloak evaluates whatever you hand it, but it doesn't source or maintain those for you. In most real ABAC systems, that attribute pipeline, not the policy language, is where the effort and bugs concentrate. This is where teams stumble in production.
Before you wire an enforcer, the Evaluate tab on the Authorization page (and the matching policy/evaluate endpoint) lets you dry-run a decision: pick a user, a resource, and a scope, and Keycloak shows the permit or deny verdict along with which policies fired. It is the fastest way to confirm a permission does what you think before any traffic hits it.
If this object model feels abstract, Keycloak now eats its own cooking. Since 26.2, Fine-Grained Admin Permissions v2 is built directly on Authorization Services and is enabled by default. The admin console's own access control becomes resources, scopes, policies, and permissions: users, groups, clients, and roles are the resources; actions such as view-members, manage-members, map-roles, and impersonate are the scopes; and who may do what is expressed as permissions over them. It is the same five nouns from this post, applied to administering Keycloak itself, which makes it a good place to see the engine in action. (See the Keycloak blog post on Fine-Grained Admin Permissions in 26.2.)
A few things regularly trip teams up. Most are accuracy issues that bite in production, not in the demo.
ENFORCING vs PERMISSIVE. This is the foot-gun that causes 3am pages. Turning on Authorization with no policies, under the default ENFORCING, denies everything. That's correct behavior, not a bug, but you need to decide this deliberately for each resource server. Many teams enable Authorization, suddenly see 403s across the board, think it's broken, and disable it. It wasn't broken. They just weren't ready.scripts feature flag (previously they loaded regardless), so the server now has to be started with scripts enabled for them to work at all. Reach for role, group, time, regex, client, and aggregate policies first, since they cover most needs without code.keycloak-policy-enforcer library, or on Quarkus with quarkus-keycloak-authorization. The evaluation engine on the server is unchanged; only the client-side enforcement library moved.Keycloak Authorization Services is a genuine policy engine hiding inside your identity provider. The model is small (Resource Server, Resource, Scope, Policy, Permission), and the decision logic is concrete code you can read: gather the policies that apply, run each through its provider, fold the results with a decision strategy, and let the enforcement mode handle the empty case.
Where does it fit?
If you are moving from "roles in a token" toward "real decisions about real resources," start by reading Keycloak 101 for the foundations and The Limits of RBAC for why the jump becomes necessary. Then model your first resource, give it one scope, write one policy, and watch the token endpoint hand back a permission.
org.keycloak.authorization source (keycloak/keycloak)Upstream Keycloak is a powerful policy engine, but managing granular resources, complex scopes, and evaluation rules at enterprise scale quickly becomes a blind spot. Keymate extends Keycloak with a production-ready, visual control plane to model, audit, and optimize your fine-grained access control in real-time.
Keymate helps teams design, deploy, and operate Keycloak Authorization Services in production, from resource modelling to policy performance under real traffic. Talk to us.
Stay updated with our latest insights and product updates