Keycloak Roles, Composite Roles, and Groups
Keycloak gives you four ways to say "this user may do that thing", and the official documentation describes all four without telling you which to pick. Here is the short version:
- Realm role — a permission that means the same thing everywhere in the realm.
employee,auditor. - Client role — a permission that only one application understands.
deployon your CI client. Lives in that client's own namespace, so two clients can both haveadmin. - Composite role — a role that contains other roles. Assign one, the user gets all of them. Use it to bundle permissions.
- Group — a collection of users that carries role mappings and attributes. Use it to organise people.
The upstream guide draws the line this way:
Composite roles apply the permission model to a set of services and applications. Use composite roles to manage applications and services. Groups focus on collections of users and their roles in an organization. Use groups to manage users.
Which is correct and hard to act on, because at the point you are choosing, both look like "a thing that holds roles". The operational version: composite roles bundle permissions, groups bundle people. If you are about to create a role named after a team, you wanted a group. If you are about to create a group named after a permission, you wanted a composite role. And if you still cannot tell, pick by what ends up in the token — which is what the rest of this page measures.
Keycloak 26.8.0 in start-dev, on 2026-10-05. Every command, token payload and byte
count below is copied from a real run. Upstream reference:
Assigning permissions using roles and groups.
Which one do I use?
| You want to… | Use | Shows in the token as |
|---|---|---|
| Gate a feature every app in the realm understands | Realm role | realm_access.roles |
| Gate a feature only one app understands | Client role | resource_access.<clientId>.roles |
| Grant 20 permissions in one click | Composite role | all 20, expanded, individually |
| Give a team shared roles and attributes | Group | nothing, by default |
| Model a reporting line or an org chart | Group hierarchy | nothing, by default |
| Hand a new user a starting set of permissions | Default roles / default groups | the roles they resolve to |
The two nothing, by default rows are the single most expensive thing on this page. Group membership does not appear in a Keycloak token unless you add a mapper for it. The roles a group grants do appear, and look exactly like directly-assigned ones. Both are demonstrated below.
Prerequisites
docker run -d --name keycloak -p 127.0.0.1:8080:8080 \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
quay.io/keycloak/keycloak:26.8.0 start-dev
Every CLI example runs kcadm.sh inside that container. A shell alias keeps the lines
short:
alias K='docker exec keycloak /opt/keycloak/bin/kcadm.sh'
K config credentials --server http://localhost:8080 \
--realm master --user admin --password admin
If you do not already have a realm, client and user, build them with
your first realm, client, and user
first. This page assumes a realm authz with a public client demo-app that has direct
access grants enabled, so we can fetch tokens with curl.
Step 1 — Realm roles and client roles
Admin console
- Realm roles → Create role → name
employee→ Save. - Repeat for
developer. - For a client role: Clients →
demo-app→ Roles tab → Create role →deploy.
kcadm.sh
K create roles -r authz -s name=employee -s 'description=Everyone in the company'
K create roles -r authz -s name=developer -s 'description=Writes code'
CID=$(K get clients -r authz -q clientId=demo-app --fields id --format csv --noquotes)
K create clients/$CID/roles -r authz -s name=deploy -s 'description=Can push to prod'
Created new role with id 'employee'
Created new role with id 'developer'
Created new role with id 'deploy'
The two namespaces are genuinely separate, and nothing stops you from putting the same name in both. Do that and the token carries both, in different places:
"realm_access": { "roles": ["employee", "deploy"] },
"resource_access": { "demo-app": { "roles": ["deploy"] } }
Those are two unrelated roles that happen to share a name. Application code that flattens all roles into one list — a very common shortcut — cannot tell them apart, and will grant the client-role permission to someone who only has the realm role. Pick a prefix convention early, or never reuse a name across namespaces.
Step 2 — Make a role composite
A composite role is any role that has other roles associated with it. Assign the composite and the user gets the lot. The inheritance is recursive: a composite of a composite works.
Admin console
- Realm roles → click
developer. - From the Action list, select Add associated roles.
- Tick
employee→ Assign.
kcadm.sh
The piece that is easy to miss: add-roles takes --rname to target a role rather than
--uusername to target a user. Same command, different flag, completely different object.
K add-roles -r authz --rname developer --rolename employee
K get roles/developer -r authz --fields name,composite
{
"name" : "developer",
"composite" : true
}
Now give a user the composite and ask for the effective mapping:
K add-roles -r authz --uusername carol --rolename staff-engineer
K get-roles -r authz --uusername carol --effective --fields name
[ { "name" : "developer" },
{ "name" : "offline_access" },
{ "name" : "uma_authorization" },
{ "name" : "employee" },
{ "name" : "staff-engineer" },
{ "name" : "default-roles-authz" } ]
One assignment, three roles: staff-engineer → developer → employee. Drop
--effective and you see only what was literally assigned. Both views are useful and they
answer different questions — "what did someone grant this person" versus "what can this
person do".
The upstream guide's own advice on composites is one sentence long and worth repeating: "we recommend that composite roles are not overused." Step 4 shows the cost in bytes.
Step 3 — Groups, subgroups, and what they carry
Groups are hierarchical: a group has many children and exactly one parent, and the path
(/engineering/platform) identifies it. A member of a child group inherits the role
mappings and the attributes of every ancestor.
Admin console
- Groups → Create group →
engineering. - Click into it → Child groups → Create group →
platform. - On
engineering: Attributes tab → adddepartment=engineering→ Save. - On
engineering: Role mapping tab → Assign role →employee. - On
platform: Role mapping → filter by clients →demo-appdeploy.
kcadm.sh
GID=$(K create groups -r authz -s name=engineering -i)
SGID=$(K create groups/$GID/children -r authz -s name=platform -i)
K update groups/$GID -r authz -s 'attributes={"department":["engineering"],"cost_center":["CC-1000"]}'
K update groups/$SGID -r authz -s 'attributes={"on_call":["true"]}'
K add-roles -r authz --gid $GID --rolename employee
K add-roles -r authz --gid $SGID --cclientid demo-app --rolename deploy
Put a user in the child group only:
BID=$(K get users -r authz -q username=bob --fields id --format csv --noquotes)
K update users/$BID/groups/$SGID -r authz -n
Bob has no role assigned to him directly, yet:
K get-roles -r authz --uusername bob --fields name # direct
K get-roles -r authz --uusername bob --effective --fields name
[ { "name" : "default-roles-authz" } ]
[ { "name" : "offline_access" },
{ "name" : "uma_authorization" },
{ "name" : "employee" },
{ "name" : "default-roles-authz" } ]
employee came from /engineering — the parent of the only group Bob joined. Inheritance
walks the whole ancestor chain, and --effective reports it. Worth knowing, because the
non-effective view makes a correctly-configured user look like they have nothing.