
For an overview of the rights model and key concepts, see Organization and Function-Based Rights Model.
Realm: orgiam
Keycloak Version: 26.5.7
2.1. Realm
2.2. Top-Level Groups
2.3. Realm Roles
2.4. The personalIdentityNumber Claim and Scope
2.4b. The phone_number Claim and Scope
2.5. The org_rights Protocol Mapper
2.6. The Admin Application Client
2.7. Example OIDC Client Registration
2.8. Example Resource Server Registration
The compose/keycloak-scripts/ directory contains wrapper scripts that automate the steps described in this document. See the
Keycloak Scripts README for usage.
Key scripts:
bootstrap-realm.sh — Creates the realm with all base configuration (sections 2.1–2.5 below)create-admin-user.sh — Creates the initial superuser accountadd-oidc-client.sh — Registers an OIDC/OAuth clientadd-resource-server.sh — Registers a resource serverset-client-functions.sh — Assigns function identifiers to a resource serverset-iam-admin-managed.sh — Marks a client as IAM-admin-managedinstall-keycloak-plugins.sh — Builds and installs Keycloak provider JARsLog in to the Keycloak Admin Console as a master realm administrator.
For the Docker test environment the admin username is
adminand the password iskeycloak.
Create a realm named orgiam. In the realm settings:
Usernames are assigned automatically by Keycloak as UUIDs. No username is collected from or exposed to the user.
See A.1, Create the Realm for details.
Create two root groups in the realm:
| Group name | Purpose |
|---|---|
orgs |
Parent for all organization groups |
functions |
Parent for all canonical function definitions |
These groups are never deleted. All organizations and functions are children of these two groups respectively.
Create a single realm-level role:
| Role | Description |
|---|---|
superuser |
Grants full access to all organizations, functions, and users in the admin application. Assigned directly to a user at the realm level, not via group membership. |
No other realm roles are used. All other authorization is derived from group membership.
personalIdentityNumber Claim and ScopeThe personal identity number is emitted by the OIDC Sweden protocol mapper
(swedish-oidc-claims-mapper), a custom mapper JAR deployed to Keycloak. The mapper is
scope-driven: it checks which client scopes were applied to the session and emits claims
accordingly. For the personal identity number, it checks for the presence of the scope
https://id.oidc.se/scope/naturalPersonNumber among the applied scopes.
The personalIdentityNumber user profile attribute is defined under
Realm settings → User Profile with:
https://id.oidc.se/scope/naturalPersonNumberClient Scope:
Create a Client Scope with the following settings:
| Setting | Value |
|---|---|
| Name | https://id.oidc.se/scope/naturalPersonNumber |
| Protocol | openid-connect |
| Include in token scope | true |
Add the OIDC Sweden mapper to this client scope:
| Setting | Value |
|---|---|
| Mapper type | OIDC Sweden |
| Name | swedish-oidc-claims-mapper |
| Add to ID token | ON |
| Add to access token | ON |
| Add to userinfo | ON |
How to attach the scope to clients:
The mapper fires only when the naturalPersonNumber scope is among the applied client
scopes for the session. This has different implications depending on the flow:
openid): Because no openid scope is
requested, optional scopes are not evaluated from the authorization request in the same
way. To ensure the personal identity number is always present in access tokens, add the
naturalPersonNumber scope as a default scope on the OAuth client. This guarantees
the scope is always applied and the mapper always fires, regardless of what API scopes
the client requested.See Sections 2.6 and 2.7 for the per-client configuration, and Appendix A for step-by-step instructions.
phone_number Claim and ScopeThe phone number is an optional user attribute delivered via the standard OIDC phone scope.
It is stored as a custom user profile attribute phoneNumber and emitted as the phone_number
claim when the phone scope is requested.
The phoneNumber user profile attribute is defined under
Realm settings → User Profile with:
phoneClient Scope:
Keycloak provides a built-in phone client scope in every realm. If it exists, configure it
as follows. If it has been removed, re-create it:
| Setting | Value |
|---|---|
| Name | phone |
| Protocol | openid-connect |
| Include in token scope | true |
Ensure the scope contains a mapper that emits the phone_number claim. The built-in scope
already includes a User Attribute mapper configured as:
| Setting | Value |
|---|---|
| Mapper type | User Attribute |
| Name | phone number |
| User Attribute | phoneNumber |
| Token Claim Name | phone_number |
| Add to ID token | ON |
| Add to access token | ON |
| Add to userinfo | ON |
If the mapper is missing or misconfigured, create or correct it with the settings above.
How to attach the scope to clients:
Add the phone scope as an optional client scope on OIDC/OAuth clients. As an optional
scope, the phone_number claim is only included in tokens when the client explicitly requests
the phone scope. If a user has no phone number set, the claim is simply absent from the token.
See A.5e for step-by-step instructions.
org_rights Protocol MapperThe org_rights claim is produced by a custom protocol mapper. This mapper must be implemented
as a deployable JAR (implementing OIDCProtocolMapper) and deployed to Keycloak’s providers
directory, or configured as a Script Mapper if scripting is enabled.
Mapper logic:
superuser realm role. If so, emit:
[{ "superuser": true }]
and stop.
Enumerate all groups the authenticated user belongs to.
Group all relevant memberships by organization identifier. For each organization:
organization_identifier, organization_name#sv,
organization_name#en.orgs/{identifier}/_admin, /_write, or /_read, add
a { "function": "*", "right": "<right>" } entry to the functions array for this org.orgs/{identifier}/{function}/_admin, /_write, or
/_read, add a { "function": "<function>", "right": "<right>" } entry.*) entry and one or more function-level entries for
the same organization. Both are emitted — consumers take the highest right across all
matching entries when evaluating access to a specific function.Mapper configuration per client:
| Client | Add to ID token | Add to access token |
|---|---|---|
https://local.dev.swedenconnect.se:17005 (IAM Admin App) |
Yes | Yes |
https://local.dev.swedenconnect.se:16990 (Demo App) |
Yes | No |
https://local.dev.swedenconnect.se:17005)The IAM Admin App serves two roles: it authenticates users via OIDC (receiving org_rights
in the ID token), and it uses a service account for Keycloak administration tasks not tied
to a specific user session.
Create a client with the following settings:
| Setting | Value |
|---|---|
| Client ID | https://local.dev.swedenconnect.se:17005 |
| Protocol | openid-connect |
| Client authentication | ON (confidential) |
| Client authenticator | Signed Jwt (private_key_jwt) |
| Standard flow | Enabled |
| Service accounts | Enabled |
| All other flows | Disabled |
iam_admin_managed attribute |
true |
Client authentication — private_key_jwt:
The client authenticates to Keycloak’s token endpoint using a signed JWT assertion. The application holds a private key; Keycloak fetches the corresponding public key from the application’s JWKS endpoint.
In the Keycloak Admin Console:
Signed Jwt →
Save.https://<host>/jwks → Save.Keycloak will fetch the public key from the JWKS URL on first use and cache it. The
application must be running and the /jwks endpoint reachable before the first token
request is made.
Service account roles (for background Keycloak admin operations):
Assign the following roles from the realm-management client to the service account:
manage-usersquery-groupsview-usersquery-usersmanage-realmview-clientsmanage-clientsProtocol mappers on this client:
org_rights mapper — ID token and access tokenpersonalIdentityNumber claim — via the https://id.oidc.se/scope/naturalPersonNumber
optional client scope (included when the client requests the scope)organization_identifier claim — via a dedicated Script or User Attribute mapper that
extracts the org identifier from the granted {org}:{function}:{right} scope; present in
access tokens onlyresource-audience-mapper — sets the aud claim based on the resource parameter and
the function from the granted scope; present in access tokens onlyScopes:
https://id.oidc.se/scope/naturalPersonNumber as an optional scope.phone as an optional scope.{org}:{function}:{right} scopes as optional scopes as they are created.Authorization behavior:
The admin application uses the org_rights claim from the ID token to determine what
organizations the user may act on. The service account is used only for background tasks
(e.g. provisioning organizations and functions) that do not require a user session.
This section shows how to register a generic OIDC/OAuth client. The Demo App
(https://local.dev.swedenconnect.se:16990) is used as a concrete example.
Create a client with the following settings:
| Setting | Value |
|---|---|
| Client ID | https://local.dev.swedenconnect.se:16990 (or your application’s base URL) |
| Protocol | openid-connect |
| Client authentication | ON (confidential) |
| Client authenticator | Signed Jwt (private_key_jwt) |
| Standard flow | Enabled |
| All other flows | Disabled |
iam_admin_managed attribute |
true (if managed by the IAM admin app) |
Client authentication — private_key_jwt:
As with the Admin Application client, this client uses private_key_jwt. Configure the
Keycloak client identically: set Client Authenticator to Signed Jwt on the
Credentials tab, then set the JWKS URL on the Keys tab to the application’s
/jwks endpoint.
Protocol mappers on this client:
org_rights mapper — ID token only (not access token)personalIdentityNumber claim — via the https://id.oidc.se/scope/naturalPersonNumber
optional client scopeorganization_identifier claim — access token only, extracted from the granted scoperesource-audience-mapper — access token only, sets aud to
[resource_server_client_id, function]Scopes:
https://id.oidc.se/scope/naturalPersonNumber as an optional scope.phone as an optional scope.{org}:{function}:{right} scopes as optional scopes as they are created.Use add-oidc-client.sh from compose/keycloak-scripts/ to automate registration. See the
Keycloak Scripts README for details.
This section shows how to register a generic OAuth resource server. The Demo Service
(https://local.dev.swedenconnect.se:16995) is used as a concrete example.
A resource server is registered in Keycloak as a client so that access tokens can carry it
as the aud claim via the resource parameter, but it has no flows, no service account,
and no Authorization Services.
Create a client with the following settings:
| Setting | Value |
|---|---|
| Client ID | https://local.dev.swedenconnect.se:16995 (or your service’s base URL) |
| Protocol | openid-connect |
| Client authentication | OFF |
| Standard flow | Disabled |
| All other flows | Disabled |
| Service accounts | Disabled |
| Authorization | Disabled |
The client_functions attribute:
If the resource server is scoped to specific functions, set the client_functions attribute
on the client. This attribute is a comma-separated list of function identifiers (e.g.,
demo or demo,sweden-connect). When the resource-aud plugin is deployed, it validates
at token issuance time that the function extracted from the requested scope matches the
client_functions attribute. If the attribute is absent or empty, the resource server is
treated as function-universal and accepts all functions.
Set the attribute using add-resource-server.sh with the --functions flag, or via
set-client-functions.sh after registration.
No protocol mappers, no client scopes, and no service account roles are needed. The service
validates incoming Bearer tokens by verifying the signature against Keycloak’s JWKS endpoint,
checking the aud claim (a multi-valued array — see the Rights Model),
and inspecting the scope and organization_identifier claims itself.
The resource-aud-plugin provides two Keycloak components that work together to handle the
OAuth 2.0 resource parameter (RFC 8707):
Resource Audience Mapper — a protocol mapper added to each OAuth client that calls
resource servers. It reads the resource parameter from the token request (or from an auth
session note if the parameter was provided on the authorization request), extracts the
function identifier from the granted scope, and sets the aud claim to a multi-valued
array: [resource_server_client_id, function]. If no resource parameter is present,
aud is set to [function]. If no org-scoped scope is present, the mapper does nothing.
The mapper is added automatically by add-oidc-client.sh to every registered OIDC/OAuth
client. It is configured with Add to access token: ON and Add to ID token: OFF.
Resource Function Executor — a Client Policy Executor that validates the resource
parameter against the target resource server’s client_functions attribute. If the
resource server does not support the function extracted from the requested scope, the
request is rejected with an invalid_target error (RFC 8707).
The executor is activated via a Client Policy profile and policy, which are created
automatically by bootstrap-realm.sh. The policy applies to all confidential clients in
the realm.
Client Policy configuration (created by bootstrap-realm.sh):
| Component | Name | Description |
|---|---|---|
| Profile | resource-function-profile |
Contains the resource-function-executor |
| Policy | resource-function-policy |
Applies the profile to all confidential clients (client-access-type = confidential) |
These can also be configured manually via the Admin Console under Realm Settings → Client Policies.
When a function is attached to an organization via the admin application, three client scopes must be created automatically (via the Admin REST API):
{org_identifier}:{function}:read
{org_identifier}:{function}:write
{org_identifier}:{function}:admin
For each scope, an Authorization Services Group Policy must be created that evaluates
membership in the qualifying groups (see the Rights Model
for the full list per right level). The policy uses Decision Strategy: AFFIRMATIVE and
Logic: POSITIVE.
A Permission must then be created linking each scope to its policy.
These policies and permissions must be created on all OIDC/OAuth clients that will
request these scopes. In the local development setup, this includes
https://local.dev.swedenconnect.se:17005 (IAM Admin App) and any other registered client.
The scopes must also be added as optional client scopes on all such clients.
This creation is the responsibility of the admin application and must be done as part of the “attach function to organization” operation.
This appendix walks through the complete initial setup of the orgiam realm, followed by
the creation of:
demo (Demo)5590026042 — Litsec ABdemo to Litsec AB196911292032 — Martin Lindström, with write on demo under Litsec ABorgiam.3.1. Display name: "Organizations and Users IAM"
3.2. HTML display name: "Organizations and Users IAM"
4.1. Add mapping between 0(key) and http://id.elegnamnden.se/loa/1.0/loa2 (value)
4.2. Add mapping between 1(key) and http://id.swedenconnect.se/loa/1.0/uncertified-loa3 (value)
4.3. Add mapping between 2(key) and http://id.elegnamnden.se/loa/1.0/loa3 (value)
4.4. Add mapping between 3(key) and http://id.elegnamnden.se/loa/1.0/loa4 (value)
ON.In Realm settings → Login:
Disable User registration
Disable Email as username
Disable Forgot password (unless needed)
In Realm settings → User profile:
Click “Create attribute”:
personalIdentityNumber.https://id.oidc.se/scope/naturalPersonNumberNote: Setting “Enabled when” to the
naturalPersonNumberscope ensures the attribute is only surfaced in tokens when the client explicitly requests that scope. The scopehttps://id.oidc.se/scope/naturalPersonNumbermust be created as a Client Scope in the realm (see A.4) before it can be referenced here.
Navigate to Groups in the left menu.
Create orgs:
orgs.Create functions:
functions.superuser Realm Rolesuperuser.Full access to all organizations, functions and users.personalIdentityNumber Client ScopePrerequisite: The
swedish-oidc-claims-mapperJAR must be deployed to Keycloak before this mapper type becomes available. Seekeycloak/swedish-oidc-claims-mapper/README.mdfor build and installation instructions.
https://id.oidc.se/scope/naturalPersonNumberOpenID ConnectONNow add the OIDC Sweden mapper:
swedish-oidc-claims-mapperONONONThe mapper reads the personalIdentityNumber user attribute and emits it as
https://id.oidc.se/claim/personalIdentityNumber whenever this scope is applied to a
session.
Note: This scope must exist in the realm before configuring the
personalIdentityNumberuser profile attribute with “Enabled when: Scopes are requested”. If you have already created the user profile attribute without this setting, go back to Realm settings → User profile → personalIdentityNumber → Enabled when and set it to this scope now.
https://local.dev.swedenconnect.se:17005OpenID Connecthttps://local.dev.swedenconnect.se:17005 and the redirect URI to /login/oauth2/code/*.Configure private_key_jwt client authentication:
This client uses signed JWT assertions for authentication rather than a client secret.
Signed Jwt →
Save.https://local.dev.swedenconnect.se:17005/jwks → Save.The application exposes its public key at /jwks. Keycloak fetches and caches the key from
this URL when processing the first token request. Ensure the application is running and the
endpoint is reachable before attempting a login.
Set the iam_admin_managed attribute:
Run the following script to mark this client as managed by the IAM admin application (see
keycloak/scripts/README.md for details):
./compose/keycloak-scripts/set-iam-admin-managed.sh <realm> \
https://local.dev.swedenconnect.se:17005 <admin-username> <admin-password>
Assign service account roles:
realm-management client.manage-users, query-groups, view-users, query-users, manage-realm, view-clients, manage-clients.Add the naturalPersonNumber scope as optional:
https://id.oidc.se/scope/naturalPersonNumber and add as Optional.Adding this scope as optional means that the personal identity number claim will be included in a token if the https://id.oidc.se/scope/naturalPersonNumber is requested.
https://local.dev.swedenconnect.se:16990)This demonstrates how to register an additional OIDC/OAuth client (the Demo App).
https://local.dev.swedenconnect.se:16990OpenID Connecthttps://local.dev.swedenconnect.se:16990 and redirect URI to /login/oauth2/code/*.Configure private_key_jwt client authentication:
This client uses signed JWT assertions for authentication rather than a client secret.
Signed Jwt →
Save.https://local.dev.swedenconnect.se:16990/jwks → Save.Set the iam_admin_managed attribute:
Run the following script to mark this client as managed by the IAM admin application (see
keycloak/scripts/README.md for details):
./compose/keycloak-scripts/set-iam-admin-managed.sh <realm> \
https://local.dev.swedenconnect.se:16990 <admin-username> <admin-password>
Add the naturalPersonNumber scope as optional:
https://id.oidc.se/scope/naturalPersonNumber and add as Optional.https://local.dev.swedenconnect.se:16995)This demonstrates how to register an OAuth resource server (the Demo Service). The Demo
Service is registered in Keycloak solely so that access tokens can carry it as the aud
claim via the resource parameter. It has no flows, no service account, and no need to
authenticate to Keycloak.
https://local.dev.swedenconnect.se:16995OpenID ConnectSet the client_functions attribute (optional):
If the resource server only supports specific functions, set the attribute using the
add-resource-server.sh script with the --functions flag. For the Demo Service, which
supports the demo function:
./compose/keycloak-scripts/add-resource-server.sh \
--realm orgiam \
--username admin \
--password keycloak \
--client-id https://local.dev.swedenconnect.se:16995 \
--name "Demo Service" \
--functions demo
naturalPersonNumber as Default Scope to https://local.dev.swedenconnect.se:16990The naturalPersonNumber scope must be added as a default scope to the Demo App client
so that the personal identity number claim is always present in access tokens requested by
this client. It is not added to 17005 as default because 17005 is an OIDC client and
already has it as optional (A.5). It is not added to 16995 at all, as that client is a
pure resource server and never requests tokens.
https://local.dev.swedenconnect.se:16990 → Client scopes tab.https://id.oidc.se/scope/naturalPersonNumber and add as Default.Because it is a default scope, Keycloak always applies it to the session regardless of what
API scopes the client requests. The OIDC Sweden mapper therefore fires unconditionally, and
the https://id.oidc.se/claim/personalIdentityNumber claim is always present in access
tokens issued by this client.
Note: Adding the scope as optional would not work here. In an OAuth 2.0 authorization request (without
openid), thescopeparameter carries only the API scopes such as5590026042:demo:write. ThenaturalPersonNumberscope would not be present in the request, so an optional scope would not be applied and the mapper would not fire. A default scope is applied by Keycloak regardless of what the client requests.
phone Scope to ClientsThe built-in phone client scope should already exist in the realm. If it does not, create it
first (see Section 4.4b).
Add to https://local.dev.swedenconnect.se:17005:
https://local.dev.swedenconnect.se:17005 → Client scopes tab.phone and add as Optional.Add to https://local.dev.swedenconnect.se:16990:
https://local.dev.swedenconnect.se:16990 → Client scopes tab.phone and add as Optional.With the scope added as optional, the phone_number claim is included in the ID token only
when the client includes phone in the scope parameter of the authorization request. If the
user has no phoneNumber attribute set, the claim is absent even when the scope is requested.
Verify the mapper:
phone_number and User Attribute phoneNumber.ON.demofunctions group.demo.demo group.name#sv = Demoname#en = Demodescription#sv = Demofunktion (longer description, optional)description#en = Demo function (longer description, optional)5590026042 — Litsec ABorgs group.5590026042.Add the following attributes: organization_identifier: 5590026042, organization_name#sv: Litsec AB and organization_name#en: Litsec AB.
Create child groups under 5590026042:
Repeat the following three times (creating _admin, _write, _read):
5590026042 group._admin (then _write, then _read).demo to Organization 5590026042demo.function_ref = demo.Create right sub-groups under 5590026042/demo:
demo sub-group (under 5590026042)._admin, _write, and _read.Create client scopes for this org/function combination:
Create three client scopes via the Admin Console or REST API (see Appendix B):
5590026042:demo:read5590026042:demo:write5590026042:demo:adminFor each scope, create an Authorization Services Group Policy with the qualifying groups as described in the Rights Model. Add each scope as an optional scope on the relevant client(s).
Create the client scope:
5590026042:demo:readOpenID ConnectONOFFRepeat for 5590026042:demo:write and 5590026042:demo:admin.
Add the scopes as optional to the relevant clients:
https://local.dev.swedenconnect.se:17005 → Client scopes tab.5590026042:demo:read and add as Optional.:write and :admin.Repeat steps 1–4 for client https://local.dev.swedenconnect.se:16990.
Enable Authorization Services on the clients:
https://local.dev.swedenconnect.se:17005 → Settings tab.Repeat for Clients → https://local.dev.swedenconnect.se:16990 → Settings tab.
Create a Group Policy for each scope:
Before creating policies and permissions, the authorization scopes must first be registered within Authorization Services (these are distinct from the client scopes created above). This must be done for both clients:
https://local.dev.swedenconnect.se:17005 → Authorization → Scopes tab.5590026042:demo:read5590026042:demo:write and 5590026042:demo:admin.Repeat steps 1–5 for Clients → https://local.dev.swedenconnect.se:16990 → Authorization → Scopes tab.
Now create a Group Policy for each:
For 5590026042:demo:read:
https://local.dev.swedenconnect.se:17005 → Authorization tab.policy-5590026042-demo-read/orgs/5590026042/_read/orgs/5590026042/_write/orgs/5590026042/_admin/orgs/5590026042/demo/_read/orgs/5590026042/demo/_write/orgs/5590026042/demo/_adminPositiveFor policy-5590026042-demo-write, add only:
/orgs/5590026042/_write/orgs/5590026042/_admin/orgs/5590026042/demo/_write/orgs/5590026042/demo/_adminFor policy-5590026042-demo-admin, add only:
/orgs/5590026042/_admin/orgs/5590026042/demo/_adminCreate a Permission linking each scope to its policy:
For 5590026042:demo:read:
https://local.dev.swedenconnect.se:17005 → Authorization tab.permission-5590026042-demo-read5590026042:demo:readpolicy-5590026042-demo-readAffirmativeRepeat for :write and :admin, linking each to its corresponding policy.
Repeat the entire Create authorization scopes, Create a Group Policy, and
Create a Permission sequence for client https://local.dev.swedenconnect.se:16990, using the
same scope names, policy names (prefixed with the client or kept identical — they are
per-client), and group lists.
A superuser is an internal system administrator. They do not need to provide a personal identity number. The username can be any chosen identifier — a name, an email address, or any other memorable string — since there is no personal identity number to use as a natural unique identifier.
Note: As with regular users, when a SAML IdP is introduced the username will become irrelevant. Superusers may however continue to use password login if they are internal administrators who are not represented in the external IdP.
diggadmin or an email address.personalIdentityNumber attribute.superuser.The user now has full access to all organizations and functions in the admin application.
Their org_rights claim will contain [{ "superuser": true }]. No personal identity number
will appear in any token issued for this user.
Note: Currently users log in with username and password. The username is set to the personal identity number so the user has something known and unique to type at the login screen. The
subclaim in tokens is always Keycloak’s internal UUID and is never derived from the username, so the personal identity number will not appear in any token claim other thanhttps://id.oidc.se/claim/personalIdentityNumber. When a SAML IdP is introduced later, the username will become irrelevant — Keycloak will identify users by matching the incoming assertion against thepersonalIdentityNumberattribute.
196911292032.MartinLindström196911292032demo under Litsec ABMartin should have write access to demo within organization 5590026042.
orgs → 5590026042 → demo → _write._write and click Join.Martin is now a member of orgs/5590026042/demo/_write. The org_rights mapper will
produce the following entry in his token:
{
"organization_identifier": "5590026042",
"organization_name#sv": "Litsec AB",
"organization_name#en": "Litsec AB",
"functions": [
{ "function": "demo", "right": "write" }
]
}
This appendix provides examples of all common operations using the Keycloak Admin REST API
against the orgiam realm.
Base URL for all Admin API calls:
https://<keycloak-host>/admin/realms/orgiam
Client authentication uses private_key_jwt. The application signs a JWT assertion with
its private key and sends it to the token endpoint. Keycloak verifies the assertion using
the public key fetched from the application’s JWKS endpoint.
The signed JWT assertion is constructed and sent automatically by the application (via Spring Security’s OAuth2 client support). For manual use or scripting, the assertion must be a JWT signed with the client’s private key and include the following claims:
| Claim | Value |
|---|---|
iss |
The client ID |
sub |
The client ID |
aud |
The token endpoint URL |
jti |
A unique identifier for this assertion |
exp |
Expiry (short-lived, typically 60 seconds) |
POST /realms/orgiam/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=https%3A%2F%2F<host>%3A<port>
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=<signed-jwt>
Use the returned access_token as a Bearer token for all subsequent Admin API calls:
Authorization: Bearer <access_token>
functions Group IDGET /admin/realms/orgiam/groups?search=functions&exact=true
Returns an array. Use the id field of the functions entry.
POST /admin/realms/orgiam/groups/<functions-group-id>/children
Content-Type: application/json
{
"name": "demo",
"attributes": {
"description#en": ["Demo function"],
"description#sv": ["Demofunktion"]
}
}
GET /admin/realms/orgiam/groups/<functions-group-id>/children
GET /admin/realms/orgiam/groups?search=demo&exact=true
orgs Group IDGET /admin/realms/orgiam/groups?search=orgs&exact=true
This requires multiple calls: create the org group, then create its three right sub-groups.
Step 1 — Create the organization group:
POST /admin/realms/orgiam/groups/<orgs-group-id>/children
Content-Type: application/json
{
"name": "5590026042",
"attributes": {
"organization_identifier": ["5590026042"],
"organization_name#sv": ["Litsec AB"],
"organization_name#en": ["Litsec AB"]
}
}
Note the id of the newly created group from the Location response header or by
subsequently searching for it.
Step 2 — Create right sub-groups:
POST /admin/realms/orgiam/groups/<org-group-id>/children
Content-Type: application/json
{ "name": "_admin" }
POST /admin/realms/orgiam/groups/<org-group-id>/children
Content-Type: application/json
{ "name": "_write" }
POST /admin/realms/orgiam/groups/<org-group-id>/children
Content-Type: application/json
{ "name": "_read" }
GET /admin/realms/orgiam/groups/<orgs-group-id>/children?briefRepresentation=false
GET /admin/realms/orgiam/groups?search=5590026042&exact=true
PUT /admin/realms/orgiam/groups/<org-group-id>
Content-Type: application/json
{
"name": "5590026042",
"attributes": {
"organization_identifier": ["5590026042"],
"organization_name#sv": ["Litsec AB — uppdaterat namn"],
"organization_name#en": ["Litsec AB — updated name"]
}
}
This requires creating the function sub-group under the org, its three right sub-groups, and the three client scopes with their Authorization policies.
Step 1 — Create function sub-group under the org:
POST /admin/realms/orgiam/groups/<org-group-id>/children
Content-Type: application/json
{
"name": "demo",
"attributes": {
"function_ref": ["demo"]
}
}
Step 2 — Create right sub-groups under the function sub-group:
POST /admin/realms/orgiam/groups/<org-function-group-id>/children
Content-Type: application/json
{ "name": "_admin" }
POST /admin/realms/orgiam/groups/<org-function-group-id>/children
Content-Type: application/json
{ "name": "_write" }
POST /admin/realms/orgiam/groups/<org-function-group-id>/children
Content-Type: application/json
{ "name": "_read" }
Step 3 — Create client scopes:
POST /admin/realms/orgiam/client-scopes
Content-Type: application/json
{
"name": "5590026042:demo:read",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "false"
}
}
Repeat for :write and :admin.
Step 4 — Create Authorization Services scopes on each client’s resource server.
Keycloak Authorization Services maintains its own scope registry per resource server, completely separate from OAuth2 client scopes. Scope permissions must reference scopes from this registry. Create each of the three scopes on both clients:
POST /admin/realms/orgiam/clients/<client-id>/authz/resource-server/scope
Content-Type: application/json
{ "name": "5590026042:demo:read" }
Repeat for :write and :admin. The response body contains the created scope with its id.
Step 5 — Create Authorization Services policies and permissions for each scope.
First, enable Authorization Services on the relevant client if not already done. Then for each scope, create a Group Policy via:
POST /admin/realms/orgiam/clients/<client-id>/authz/resource-server/policy/group
Content-Type: application/json
{
"name": "policy-5590026042-demo-read",
"groups": [
{ "path": "/orgs/5590026042/_read", "extendChildren": false },
{ "path": "/orgs/5590026042/_write", "extendChildren": false },
{ "path": "/orgs/5590026042/_admin", "extendChildren": false },
{ "path": "/orgs/5590026042/demo/_read", "extendChildren": false },
{ "path": "/orgs/5590026042/demo/_write", "extendChildren": false },
{ "path": "/orgs/5590026042/demo/_admin", "extendChildren": false }
],
"logic": "POSITIVE",
"decisionStrategy": "AFFIRMATIVE"
}
For :write, include only _write and _admin groups (at both levels).
For :admin, include only _admin groups (at both levels).
Note: Unlike group and scope creation endpoints, the Authorization Services policy and permission endpoints do not return a
Locationheader. Instead, they return201 Createdwith the created resource as a JSON body. Extract theidfield from the response body to obtain the policy ID.
Then create the corresponding scope permission:
POST /admin/realms/orgiam/clients/<client-id>/authz/resource-server/permission/scope
Content-Type: application/json
{
"name": "permission-5590026042-demo-read",
"type": "scope",
"scopes": ["5590026042:demo:read"],
"policies": ["<policy-id>"],
"decisionStrategy": "AFFIRMATIVE"
}
Note: The
scopesarray must contain the scope name (e.g."5590026042:demo:read"), not the scope UUID. This is inconsistent with thepoliciesfield which takes a UUID, but it is how the Keycloak Authorization Services API works.
Again, the permission id is in the response body, not a Location header.
Repeat the policy + permission pair for :write and :admin scopes, using the appropriate
group lists and scope names.
Step 6 — Add scopes as optional to relevant clients:
PUT /admin/realms/orgiam/clients/<client-id>/optional-client-scopes/<scope-id>
When creating a user via the REST API, omit the username field to let Keycloak generate a
UUID, or supply a UUID explicitly. The personal identity number is stored as an attribute only.
POST /admin/realms/orgiam/users
Content-Type: application/json
{
"enabled": true,
"firstName": "Martin",
"lastName": "Lindström",
"attributes": {
"personalIdentityNumber": ["196911292032"]
}
}
Note: Keycloak requires the
usernamefield in some versions even when UUID generation is intended. If the API rejects the request, supply an explicit UUID:"username": "<generated-uuid>".
GET /admin/realms/orgiam/users?q=personalIdentityNumber:196911292032&exact=true
GET /admin/realms/orgiam/users/<user-id>
PUT /admin/realms/orgiam/users/<user-id>
Content-Type: application/json
{
"firstName": "Martin",
"lastName": "Lindström",
"attributes": {
"personalIdentityNumber": ["196911292032"]
}
}
GET /admin/realms/orgiam/users?max=100
GET /admin/realms/orgiam/groups?search=_write&exact=true
Alternatively, traverse the tree:
GET /admin/realms/orgiam/groups?search=5590026042&exact=true
Then navigate into sub-groups using the returned subGroupCount or:
GET /admin/realms/orgiam/groups/<org-group-id>/children
Add the user to the appropriate right group. For write on demo under 5590026042:
PUT /admin/realms/orgiam/users/<user-id>/groups/<_write-group-id-under-demo>
No body is required. A 204 No Content response indicates success.
DELETE /admin/realms/orgiam/users/<user-id>/groups/<group-id>
GET /admin/realms/orgiam/users/<user-id>/groups
Returns all groups the user belongs to, including path information.
superuser Role to a UserFirst, retrieve the superuser role ID:
GET /admin/realms/orgiam/roles/superuser
Then assign it:
POST /admin/realms/orgiam/users/<user-id>/role-mappings/realm
Content-Type: application/json
[
{
"id": "<superuser-role-id>",
"name": "superuser"
}
]
superuser Role from a UserDELETE /admin/realms/orgiam/users/<user-id>/role-mappings/realm
Content-Type: application/json
[
{
"id": "<superuser-role-id>",
"name": "superuser"
}
]
superuser RoleGET /admin/realms/orgiam/users/<user-id>/role-mappings/realm
Look for superuser in the returned array.
GET /admin/realms/orgiam/groups/<org-group-id>/children
Filter out _admin, _write, _read from the results — the remaining children are the
attached function sub-groups.
There is no direct reverse-lookup in Keycloak’s group API. The admin application must fetch all organizations and filter client-side:
GET /admin/realms/orgiam/groups/<orgs-group-id>/children?briefRepresentation=false
Then for each organization, check if a child group exists with the desired function name.
GET /admin/realms/orgiam/groups/<group-id>/members
For example, to list all users with write right on demo under 5590026042, obtain
the ID of orgs/5590026042/demo/_write and call the members endpoint.
Organization mutable attributes (names, contact info) are stored on the org group
representation and updated via a PUT to the group endpoint. The full group representation
must be supplied — Keycloak replaces the entire object, so always fetch first and merge.
GET /admin/realms/orgiam/groups/<org-group-id>
PUT /admin/realms/orgiam/groups/<org-group-id>
Content-Type: application/json
{
"id": "<org-group-id>",
"name": "5590026042",
"attributes": {
"organization_identifier": ["5590026042"],
"organization_name#sv": ["Litsec AB"],
"organization_name#en": ["Litsec AB"],
"contact_info": ["{\"email\":\"info@litsec.se\",\"phone_number\":\"+46701234567\"}"]
}
}
The contact_info attribute is a single-element list containing a compact JSON string with
the optional members email and phone_number. Omit the attribute entirely if no contact
details are set. Always carry forward the existing organization_identifier and
organization_name#* attributes when only updating contact info, and vice versa — the PUT
replaces all attributes.
A 204 No Content response indicates success.
PUT /admin/realms/orgiam/users/<user-id>
Content-Type: application/json
{
"firstName": "Martin",
"lastName": "Lindström",
"email": "martin@example.com",
"attributes": {
"personalIdentityNumber": ["196911292032"],
"phoneNumber": ["+46701234567"]
}
}
As with group updates, always fetch the current user representation first and merge the
changed fields. This preserves personalIdentityNumber and any other attributes not
being modified. Omit phoneNumber from the attributes map (or remove the key entirely)
to clear the phone number. Never modify personalIdentityNumber via this endpoint.
A 204 No Content response indicates success.
DELETE /admin/realms/orgiam/users/<user-id>
This permanently removes the user from the realm, including all group memberships. It cannot be undone. Before calling this endpoint, the admin application must verify that the user is not the last administrator for any organization or function.
A 204 No Content response indicates success.
Copyright © 2026, Myndigheten för digital förvaltning - Swedish Agency for Digital Government (DIGG). Licensed under version 2.0 of the Apache License.