Keycloak SCIM API: Enable and Authenticate with Client Credentials

Tested on Red Hat Enterprise Linux 10.2 (Coughlan)
Package podman-5.8.2-5.el10_2.x86_64
quay.io/keycloak/keycloak:26.7.4
curl-8.12.1-4.el10_2.4.x86_64
jq-1.7.1-11.el10_2.2.x86_64
Applies to Any Linux host with Podman or Docker
Privilege Normal user (rootless Podman). Realm setup uses the Keycloak bootstrap admin
Scope Enable the native Keycloak SCIM 2.0 API, turn it on for a realm, authenticate with a client-credentials token, set the SCIM audience, and verify a read with curl. User and group provisioning, and Microsoft Entra, are separate tasks.
Related guides Keycloak OpenID Connect tutorial
Keycloak client scopes
Realm role vs client role
Keycloak bootstrap admin
Keycloak container persistence
Short on time?Beta Get the gist of this 21-minute read in seconds, then ask follow-up questions

Keycloak 26.7.4 includes a native SCIM 2.0 API for realm users and groups. The API is a Preview feature and stays off until you start the server with --features=scim-api and turn SCIM on for the realm.

This guide uses a confidential service-account client with the OAuth client credentials grant. Its access token must include the realm's SCIM base URL in the aud claim. The curl checks below show the 404, Bearer token required, and Invalid token audience responses that appear when one of those steps is missing.


Does Keycloak Support SCIM?

Yes. Keycloak 26.7.4 includes a SCIM 2.0 API. Before enabling SCIM, there are four things you need to know:

  • The feature is Preview and disabled in the default server profile. Pass --features=scim-api. Do not enable the whole preview set.
  • Keycloak is the SCIM service provider. An external system is the client and sends requests in. This release does not push Keycloak users out to another SCIM server.
  • The feature flag does not authorize callers. Each realm has its own SCIM switch, and every request needs an OAuth bearer token whose aud claim is that realm's SCIM base URL.
  • The Admin REST API (/admin/realms/.../users) is a different interface. A client that can call the Admin API is not automatically speaking SCIM.

How Keycloak SCIM Works

Keycloak is the SCIM service provider

SCIM (System for Cross-domain Identity Management) is a standard API protocol for managing identities between systems. A SCIM client can use it to read, create, update, or remove users and groups without relying on a product-specific user-management API.

In SCIM, the service provider exposes the API and the user or group resources being managed. The SCIM client calls that API. RFC 7644 is the protocol.

SCIM's term service provider is unrelated to a SAML Service Provider. Here it simply means the system exposing the SCIM API.

External SCIM client
Entra / IGA / HR / curl
        |
        | SCIM request
        | Authorization: Bearer <token>
        v
┌────────────────────────────┐
│ Keycloak                   │
│ SCIM service provider      │
│ /realms/<realm>/scim/v2    │
└────────────┬───────────────┘
             |
             v
      Realm users/groups

Keycloak is the service provider, so it exposes the API being called. The external system is the SCIM client. In Keycloak 26.7.x that direction is inbound: another system manages users and groups in Keycloak through SCIM. Products such as Microsoft Entra can be that client. Configuring Entra itself is a separate task.

For the vocabulary behind service provider and identity provider, see IdP vs service provider.

SCIM provisioning vs authentication

SCIM manages the account. It does not log the user in. User authentication still happens separately through whatever login method Keycloak is configured to use, such as OIDC or SAML federation, passwords, or WebAuthn. LDAP vs SAML vs OIDC vs SCIM separates those jobs.


Enable the Keycloak SCIM API

Two switches have to be on before a SCIM URL exists. The server feature exposes the routes. The realm switch publishes them for that realm. Think of the server flag as enabling SCIM support in this Keycloak process, while scimApiEnabled decides which individual realms are allowed to expose that API.

When this section is done you will have:

  • Container keycloak-scim-api on 127.0.0.1:8080, image 26.7.4, with only scim-api added
  • Realm golinuxcloud with the SCIM API switch on
  • A live SCIM URL that answers 401 until you send a bearer token
IMPORTANT
In Keycloak 26.7.4 the SCIM API is a Preview feature. It is disabled by default, and preview behavior can change in a later minor release. Enable scim-api on a lab realm before a provisioning connector depends on it.

Container commands use the podman command. If Podman is not installed yet, follow Install Keycloak with Podman and then come back. Pin 26.7.4. Do not use the latest tag. This container uses start-dev, so realm data lives inside the container filesystem. Removing the container drops the realm.

Enable the scim-api server feature

First, make one request with SCIM disabled. This gives you the exact 404 response to compare against later if SCIM does not come up correctly. Start without the feature:

bash
podman run -d --name keycloak-scim-api \
  -p 127.0.0.1:8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD='ChangeMe!2026' \
  quay.io/keycloak/keycloak:26.7.4 start-dev
output
0289a3b57693ada1fdbedf612ca0c1f7ff88ccadb7abdd2d4075564774f50630

podman run -d prints the container id and returns. First boot often takes 30–90 seconds. Wait until the log shows that Keycloak is listening, and check that the preview feature line is absent. Filter with the grep command:

bash
podman logs keycloak-scim-api 2>&1 | grep -E 'Preview features enabled|Listening on'
output
2026-10-02 16:23:54,377 INFO  [io.quarkus] (main) Keycloak 26.7.4 on JVM (powered by Quarkus 3.33.3.2) started in 25.398s. Listening on: http://0.0.0.0:8080

There is no Preview features enabled: scim-api line. Listening on: http://0.0.0.0:8080 is the address inside the container. The -p 127.0.0.1:8080:8080 publish still limits the host to loopback.

Ask for the SCIM service-provider document on the master realm. Use the built-in master realm only for this first server-level test, because golinuxcloud has not been created yet. All remaining SCIM configuration is done in golinuxcloud. Use the curl command and keep the status code in the transcript:

bash
curl -sS -w '\nHTTP %{http_code}\n' -H 'Accept: application/scim+json' \
  http://127.0.0.1:8080/realms/master/scim/v2/ServiceProviderConfig
output
{"error":"HTTP 404 Not Found"}
HTTP 404

That body is Keycloak's generic not-found page, not a SCIM error document. The route was never registered.

IMPORTANT
The next command deletes container keycloak-scim-api. This first container has no realm yet. The same command later also deletes realm golinuxcloud, because start-dev stores data inside the container.

Remove this container:

bash
podman rm -f keycloak-scim-api
output
keycloak-scim-api

podman rm -f prints the name it removed. Start the replacement with the feature flag after the image name and the start-dev command:

bash
podman run -d --name keycloak-scim-api \
  -p 127.0.0.1:8080:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
  -e KC_BOOTSTRAP_ADMIN_PASSWORD='ChangeMe!2026' \
  quay.io/keycloak/keycloak:26.7.4 start-dev --features=scim-api
output
aa09f88320ceb00ba9b27dc9845d63312904ac50f7ca505a2d9bac612cea084c

Wait for listen again. This time the preview line should be present:

bash
podman logs keycloak-scim-api 2>&1 | grep -E 'Preview features enabled|Listening on'
output
2026-10-02 16:53:56,863 INFO  [org.keycloak.common.Profile] (build-4) Preview features enabled: scim-api:v1
2026-10-02 16:54:13,001 INFO  [org.keycloak.common.Profile] (main) Preview features enabled: scim-api:v1
2026-10-02 16:54:25,364 INFO  [io.quarkus] (main) Keycloak 26.7.4 on JVM (powered by Quarkus 3.33.3.2) started in 20.331s. Listening on: http://0.0.0.0:8080

scim-api:v1 is loaded. Keycloak prints it once at build and once at start. Confirm HTTP on the host before you create a realm:

bash
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/realms/master
output
200

HTTP 200 means the master realm document is being served. The SCIM feature is on, but each realm still defaults to off.

Enable SCIM for the realm

Log kcadm.sh into the container. It stores that session under /opt/keycloak/.keycloak/ inside the container, so later kcadm.sh commands reuse it:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh config credentials \
  --server http://127.0.0.1:8080 --realm master --user admin \
  --password 'ChangeMe!2026'
output
Logging into http://127.0.0.1:8080 as user admin of realm master

That line is the bootstrap admin on the master realm, from inside the container.

Create a realm named golinuxcloud with SCIM still off, so the second 404 is visible:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh create realms \
  -s realm=golinuxcloud -s enabled=true -s scimApiEnabled=false
output
Created new realm with id 'golinuxcloud'

Read the flag back:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh get realms/golinuxcloud \
  --fields realm,scimApiEnabled
output
{
  "realm" : "golinuxcloud",
  "scimApiEnabled" : false
}

The realm exists and SCIM is still off. Call its SCIM URL:

bash
curl -sS -w '\nHTTP %{http_code}\n' -H 'Accept: application/scim+json' \
  http://127.0.0.1:8080/realms/golinuxcloud/scim/v2/ServiceProviderConfig
output
{"error":"HTTP 404 Not Found"}
HTTP 404

Same generic 404 as before. The feature flag alone did not publish the realm.

Turn the realm switch on in the Admin Console at http://127.0.0.1:8080/admin/:

  1. Sign in as admin and pick golinuxcloud in the realm selector.
  2. Open Realm settings and stay on the General tab.
  3. Scroll to SCIM API, switch it to On, and click Save.

In Keycloak 26.7.4, the SCIM API toggle appears near the bottom of Realm settings → General. It only appears when the server started with --features=scim-api:

Keycloak 26.7.4 Realm settings General tab with the SCIM API toggle switched On for the golinuxcloud realm

After Save, Keycloak reports that the realm was updated. Nothing else on the page changes.

The same switch from the shell is one kcadm.sh update. It prints nothing when the write succeeds:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh update realms/golinuxcloud \
  -s scimApiEnabled=true

Read the flag again:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh get realms/golinuxcloud \
  --fields realm,scimApiEnabled
output
{
  "realm" : "golinuxcloud",
  "scimApiEnabled" : true
}

scimApiEnabled is true. Both switches are on.

Why Keycloak SCIM Returns 404

In this 26.7.4 test, the generic {"error":"HTTP 404 Not Found"} response meant the SCIM route had not been exposed yet. Both the server feature and the realm-level SCIM switch had to be enabled before the same URL moved to a SCIM-formatted 401.

I hit both failure states before the route existed:

  • Feature off, on the master realm: HTTP 404 and {"error":"HTTP 404 Not Found"}.
  • Feature on, realm golinuxcloud still at scimApiEnabled=false: the same generic 404.
  • Both on: the next call, with no token, is HTTP 401 and a SCIM error document.

Verify the SCIM route

Repeat the same URL with no token:

bash
curl -sS -w '\nHTTP %{http_code}\n' -H 'Accept: application/scim+json' \
  http://127.0.0.1:8080/realms/golinuxcloud/scim/v2/ServiceProviderConfig
output
{"schemas":["urn:ietf:params:scim:api:messages:2.0:Error"],"status":"401","detail":"Bearer token required"}
HTTP 401

HTTP 401 and a SCIM error schema mean the route is live and wants a bearer token. detail is Bearer token required.


Find the Keycloak SCIM Base URL

This realm uses two URLs. Replace the host, port, and realm name when yours differ.

text
SCIM API:
http://127.0.0.1:8080/realms/golinuxcloud/scim/v2

Token endpoint:
http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/token

The client sends its ID and secret to the token endpoint. It sends the resulting bearer token to the SCIM endpoint. These URLs have different jobs: the token endpoint authenticates the client, and the SCIM endpoint receives the API request. The SCIM path is /realms/{realm}/scim/v2, with no /auth prefix and no /admin.

Keycloak publishes these paths under that base:

Path Role
/ServiceProviderConfig Capability flags: patch, filter, bulk, sort, password change, etag
/ResourceTypes Which resources exist. On this server, User and Group
/Schemas Attribute schemas for those resource types
/Users User resources
/Groups Group resources

None of these paths are anonymous. The 401 above was /ServiceProviderConfig with both switches already on.

Do not look for the base URL under Endpoints. In 26.7.4, Realm settings → General → Endpoints lists only OpenID Endpoint Configuration and SAML 2.0 Identity Provider Metadata. There is no SCIM link, so build the URL from the realm name yourself.

The audience must use the same SCIM base URL that Keycloak expects for the request, including scheme, host, port, and path. In this local lab, localhost and 127.0.0.1 are therefore different audience values.

If Keycloak sits behind a reverse proxy, use the SCIM base URL corresponding to the frontend URL seen by the client. For example, if users reach Keycloak at https://sso.example.com, do not blindly use an internal address such as http://keycloak:8080 as the audience.


Authenticate to the Keycloak SCIM API

This guide uses the OAuth client credentials grant, so scim-client must be a confidential client with client authentication and service accounts enabled.

Getting a Keycloak access token is not enough by itself. The token must also identify the realm's SCIM API as an intended audience.

Client ID + client secret
         |
         v
Keycloak token endpoint
         |
         | OAuth access token
         v
Access token contains
the SCIM base URL in aud
         |
         v
Keycloak SCIM API
         |
         +-- wrong or missing aud --> 401 Invalid token audience
         |
         +-- correct aud -----------> request is evaluated

Create a confidential service-account client

A confidential client can authenticate itself to Keycloak using a credential such as a client secret. That is why enabling Client authentication creates the Credentials tab used in the next step.

Create the client in the Admin Console, still in realm golinuxcloud:

  1. Open Clients and click Create client.
  2. Leave Client type as OpenID Connect, set Client ID to scim-client, and click Next.
  3. On Capability config, switch Client authentication to On.
  4. Under Authentication flow, clear Standard flow and check Service account roles. Leave Direct access grants cleared.
  5. Click Next, leave the login settings empty, and click Save.

The capability page should look like this before you click Next. This client uses the OAuth client credentials grant, so it does not need browser-based login flows. Leave Service account roles enabled and the interactive authentication flows disabled for this setup:

Keycloak Create client Capability config with Client authentication On and only Service account roles checked under Authentication flow

Keycloak opens the new client's details page and reports that the client was created.

Open the Credentials tab. Keycloak generated a secret for the client. The eye icon reveals it, and the copy icon copies it:

Keycloak scim-client Credentials tab with Client Id and Secret authenticator and the masked Client Secret field

The secret stays masked until you click the eye icon. Treat the secret like a password. Anyone who obtains the client ID and secret can authenticate as this service account and request tokens carrying its assigned permissions.

The curl commands below read the secret from CLIENT_SECRET. If the console generated the secret, paste that value. The shell command in the next paragraph sets a fixed lab secret for the CLI path.

If you would rather script the client, kcadm.sh builds the same thing and sets the secret to scim-lab-secret-2026:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh create clients -r golinuxcloud \
  -s clientId=scim-client \
  -s enabled=true \
  -s publicClient=false \
  -s secret=scim-lab-secret-2026 \
  -s serviceAccountsEnabled=true \
  -s standardFlowEnabled=false \
  -s directAccessGrantsEnabled=false \
  -s implicitFlowEnabled=false
output
Created new client with id 'da72d07c-66a9-4717-bbc9-b699ab6ba3ff'

Store the secret for the token requests. Replace the value if you copied a generated secret from the Credentials tab:

bash
CLIENT_SECRET='scim-lab-secret-2026'

The assignment prints nothing.

Assign SCIM read permissions

A Keycloak service account represents the client as a non-human identity. When scim-client uses the client credentials grant, Keycloak evaluates the roles assigned to this service-account user when deciding what the token can do. Keycloak created this user automatically as service-account-scim-client.

The Keycloak 26.7 documentation separates SCIM permissions by operation:

Need Role
Discovery endpoints query-users or query-groups
Search or list users query-users
Read a user's details view-users
Create, update, or delete users and groups manage-users

This guide assigns view-users because the later read retrieves a full user resource. view-users is composite, so the token also receives the query roles needed for discovery calls. With only query-users, the list returned a total and an empty Resources array, and GET by id returned 403.

NOTE
Some earlier Keycloak SCIM material mentions view-realm for discovery endpoints. In this tested 26.7.4 setup, view-realm alone returned 403, while query-users and query-groups matched the 26.7 server administration permission model.

Assign view-users in the Admin Console:

  1. On the client, open the Service account roles tab.
  2. Click Assign role and choose Client roles.
  3. Search for view-users, check the row whose Client ID is realm-management, and click Assign.

The tab then lists the new mapping next to the default realm role:

Keycloak scim-client Service account roles tab listing realm-management view-users next to default-roles-golinuxcloud

Inherited reads False because you assigned the role directly. view-users is a composite role. Its included query-* roles are hidden here because Hide inherited roles is selected.

From the shell, grant the same role. add-roles prints nothing when the mapping is stored:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh add-roles -r golinuxcloud \
  --uusername service-account-scim-client \
  --cclientid realm-management \
  --rolename view-users

Get an access token with client credentials

Request a client-credentials token and keep the raw access token out of the transcript. This first token has no SCIM audience yet:

bash
curl -sS -X POST http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/token \
  -d 'grant_type=client_credentials' \
  -d 'client_id=scim-client' \
  -d "client_secret=$CLIENT_SECRET" \
  | jq '{token_type, expires_in, scope}'
output
{
  "token_type": "Bearer",
  "expires_in": 300,
  "scope": "profile email"
}

The client authenticated. The access token lives 300 seconds. aud means audience. It identifies which API or service is intended to accept the token. Decode the payload and print only aud and the realm-management roles:

bash
curl -sS -X POST http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/token \
  -d 'grant_type=client_credentials' \
  -d 'client_id=scim-client' \
  -d "client_secret=$CLIENT_SECRET" \
  | jq -r '.access_token | split(".")[1] | gsub("-";"+") | gsub("_";"/") | . + "==" | @base64d | fromjson | {azp, aud, roles: .resource_access["realm-management"].roles}'
output
{
  "azp": "scim-client",
  "aud": [
    "realm-management",
    "account"
  ],
  "roles": [
    "view-users",
    "query-groups",
    "query-users"
  ]
}

view-users is on the token, along with query-users and query-groups. aud lists realm-management and account. It does not list the SCIM base URL.

Fix 401 Invalid token audience in Keycloak SCIM

Keycloak issued the token successfully. SCIM still rejects it, because the token does not identify the SCIM API as an intended recipient.

The current aud is:

  • realm-management
  • account

The required aud also includes:

  • http://127.0.0.1:8080/realms/golinuxcloud/scim/v2

Store a token and call ServiceProviderConfig with it:

bash
ACCESS_TOKEN=$(curl -sS -X POST http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/token \
  -d 'grant_type=client_credentials' \
  -d 'client_id=scim-client' \
  -d "client_secret=$CLIENT_SECRET" \
  | jq -r '.access_token')

The assignment prints nothing. Send that value as a bearer token:

bash
curl -sS -w '\nHTTP %{http_code}\n' \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H 'Accept: application/scim+json' \
  http://127.0.0.1:8080/realms/golinuxcloud/scim/v2/ServiceProviderConfig
output
{"schemas":["urn:ietf:params:scim:api:messages:2.0:Error"],"status":"401","detail":"Invalid token audience"}
HTTP 401

Invalid token audience means the secret and the role were fine. The aud claim did not contain the SCIM base URL.

Here, the audience identifies the API that should accept the token, not the client that requested it. scim-client asks Keycloak for the token, while the realm's SCIM base URL identifies the API that the token will be sent to. That is why the SCIM URL must appear in aud.

Add the SCIM audience to the token

Add an Audience protocol mapper on the client's dedicated scope:

  1. On scim-client, open the Client scopes tab and click scim-client-dedicated.
  2. Click Configure a new mapper and pick Audience from the list.
  3. Set Name to scim-audience.
  4. Leave Included Client Audience empty. Set Included Custom Audience to http://127.0.0.1:8080/realms/golinuxcloud/scim/v2.
  5. Keep Add to access token on. Add to ID token can stay off, because SCIM only reads the access token.
  6. Click Save.

Two fields matter here:

  • Included Custom Audience must be the full SCIM base URL.
  • Add to access token must remain On.

Keycloak Add mapper form for an Audience mapper with Included Custom Audience set to the golinuxcloud SCIM base URL and Add to access token On

After Save, the dedicated scope lists scim-audience with type Audience under Mappers.

On the CLI, the mapper goes on the client by its internal id. Read the id first. Yours will differ from the sample:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh get clients -r golinuxcloud \
  -q clientId=scim-client --fields id --format csv --noquotes
output
da72d07c-66a9-4717-bbc9-b699ab6ba3ff

That single line is the id. Capture it for the mapper create. The command prints nothing:

bash
CLIENT_UUID=$(podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh get clients -r golinuxcloud -q clientId=scim-client --fields id --format csv --noquotes)

Create the mapper. included.custom.audience is the SCIM base URL, and the claim is added to the access token:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh create \
  "clients/${CLIENT_UUID}/protocol-mappers/models" -r golinuxcloud \
  -s name=scim-audience \
  -s protocol=openid-connect \
  -s protocolMapper=oidc-audience-mapper \
  -s 'config."included.custom.audience"=http://127.0.0.1:8080/realms/golinuxcloud/scim/v2' \
  -s 'config."access.token.claim"=true' \
  -s 'config."id.token.claim"=false'
output
Created new model with id '2c5f3542-2c69-4e14-a97b-d3c28928dde2'

Request another token and read aud again. Access tokens are already-signed documents. Changing a mapper does not rewrite tokens that were issued earlier. Request a new token so the new aud claim is included:

bash
curl -sS -X POST http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/token \
  -d 'grant_type=client_credentials' \
  -d 'client_id=scim-client' \
  -d "client_secret=$CLIENT_SECRET" \
  | jq -r '.access_token | split(".")[1] | gsub("-";"+") | gsub("_";"/") | . + "==" | @base64d | fromjson | {azp, aud}'
output
{
  "azp": "scim-client",
  "aud": [
    "http://127.0.0.1:8080/realms/golinuxcloud/scim/v2",
    "realm-management",
    "account"
  ]
}

The first aud value is the SCIM base URL. The next section sends a token like this.


Test the Keycloak SCIM API with curl

The realm has no ordinary user yet. The service-account user does not show up in a SCIM user list. Add one local account named ada so the read has a row to return. Passwords and required actions are covered in Keycloak users. From the shell:

bash
podman exec keycloak-scim-api /opt/keycloak/bin/kcadm.sh create users -r golinuxcloud \
  -s username=ada -s enabled=true -s firstName=Ada -s lastName=Lovelace \
  -s email=ada@example.com -s emailVerified=true
output
Created new user with id 'f4ce7596-5fee-4708-bdae-668cea31c62d'

ada exists in the realm. That create was the Admin API, not SCIM. Create the user this way only to give the read-only SCIM client something safe to retrieve. SCIM user creation belongs in a separate provisioning tutorial. Take a fresh access token in this shell so this section does not depend on an earlier one:

bash
ACCESS_TOKEN=$(curl -sS -X POST http://127.0.0.1:8080/realms/golinuxcloud/protocol/openid-connect/token \
  -d 'grant_type=client_credentials' \
  -d 'client_id=scim-client' \
  -d "client_secret=$CLIENT_SECRET" \
  | jq -r '.access_token')

The assignment prints nothing on success.

Test ServiceProviderConfig

ServiceProviderConfig is a SCIM metadata endpoint. It tells a client which optional SCIM capabilities this server supports. Read the capability flags:

bash
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Accept: application/scim+json' \
  http://127.0.0.1:8080/realms/golinuxcloud/scim/v2/ServiceProviderConfig \
  | jq '{patch, bulk, filter, changePassword, sort, etag, auth: [.authenticationSchemes[].type]}'
output
{
  "patch": {
    "supported": true
  },
  "bulk": {
    "supported": false,
    "maxOperations": 0,
    "maxPayloadSize": 0
  },
  "filter": {
    "supported": true,
    "maxResults": 100
  },
  "changePassword": {
    "supported": false
  },
  "sort": {
    "supported": false
  },
  "etag": {
    "supported": false
  },
  "auth": [
    "oauthbearertoken"
  ]
}

The token was accepted. Patch and filter are supported. Bulk, password change, sort, and etag are not. The only authentication scheme is an OAuth bearer token.

Test GET /Users

Read users and ask for up to 10 rows:

bash
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Accept: application/scim+json' \
  'http://127.0.0.1:8080/realms/golinuxcloud/scim/v2/Users?count=10' \
  | jq '{totalResults, startIndex, itemsPerPage, Resources: [.Resources[] | {id, userName, active, name, emails}]}'
output
{
  "totalResults": 1,
  "startIndex": 1,
  "itemsPerPage": 1,
  "Resources": [
    {
      "id": "f4ce7596-5fee-4708-bdae-668cea31c62d",
      "userName": "ada",
      "active": true,
      "name": {
        "formatted": "Ada Lovelace",
        "familyName": "Lovelace",
        "givenName": "Ada"
      },
      "emails": [
        {
          "value": "ada@example.com",
          "type": "work",
          "primary": true
        }
      ]
    }
  ]
}

userName is ada, and the id matches the id from kcadm.sh. active: true reflects that the Keycloak user is enabled. The Keycloak email is represented as a primary work entry in SCIM's emails array. That is a successful authenticated read.

The SCIM endpoint is enabled and the token was accepted. view-users is enough for this read. Write operations need additional permissions, such as manage-users. If the next job is provisioning or Microsoft Entra, treat that as its own configuration. A missing SCIM audience still fails with Invalid token audience after a role change, so keep the mapper.


Keycloak SCIM 404, 401 and 403 Errors

Read the status on GET /scim/v2/ServiceProviderConfig before changing roles or mappers. Each response helps narrow down which stage of the setup still needs attention:

GET /scim/v2/ServiceProviderConfig
              |
              v
             404
              |
              +-- feature or realm SCIM is off
              |
              v
   401 Bearer token required
              |
              +-- token missing
              |
              v
  401 Invalid token audience
              |
              +-- fix the audience
              |
              v
        token accepted
         /            \
insufficient role   sufficient role
       |                  |
      403                200
Symptom Likely cause Fix
HTTP 404 and {"error":"HTTP 404 Not Found"} scim-api is off, or the realm switch is off Start with --features=scim-api and set scimApiEnabled=true. In this 26.7.4 test, the same URL then returned a SCIM 401.
HTTP 401 Bearer token required No Authorization header Send a client-credentials token as Authorization: Bearer.
HTTP 401 Invalid token audience aud lacks the realm SCIM base URL Set the Audience mapper to http://<host>:<port>/realms/<realm>/scim/v2 and request a new token.
HTTP 403 on ServiceProviderConfig The token lacks query-users or query-groups. view-realm alone was insufficient in this 26.7.4 test Assign view-users. It includes both query roles.
totalResults is set but Resources is empty, or GET by id is 403 Only query-users is granted Assign view-users.
HTTP 403 on a write The token can read Assign manage-users for create, update, patch, and delete.

Keycloak SCIM Limitations in 26.7.4

These are the limits that matter before you point another system at the API. Preview behavior can change in a later minor release.

  • Users and Groups are the resource types. ServiceProviderConfig is read-only. You cannot PATCH it to turn bulk on.
  • PATCH and filter are supported. filter.maxResults is 100.
  • Bulk, SCIM password change, sort, and ETag are not supported. Set passwords in Keycloak.
  • Users and groups who hold administrative roles are only partly visible over SCIM, and SCIM writes against them are refused. Change those accounts in the Admin Console or the Admin API.
  • Keycloak 26.7.4 acts as the SCIM service provider in this flow. External clients call Keycloak. It does not act as an outbound SCIM client that provisions its users into another provider.
  • POST /admin/realms/golinuxcloud/users is the Admin REST API. Similar JSON does not make that call SCIM.

Summary

Keycloak 26.7.4 exposes SCIM 2.0 only after two settings are enabled. The server must start with --features=scim-api, and the target realm must have scimApiEnabled=true.

For the read-only client used here, enable client authentication and service accounts, assign view-users, and obtain a token through the client credentials grant. The token's aud claim must contain the complete SCIM base URL, or Keycloak returns 401 Invalid token audience.

Once those pieces are in place, ServiceProviderConfig and GET /Users are simple checks that the SCIM endpoint, authentication, audience, and read permission are working. Other systems call Keycloak. This release does not push users out to an external SCIM server.


References


Frequently Asked Questions

Does Keycloak support SCIM 2.0?

Yes, in Keycloak 26.7.4, as a Preview feature. Start the server with --features=scim-api, and turn the realm's SCIM API switch on. Keycloak then exposes SCIM 2.0 under /realms/{realm}/scim/v2.

Why does the Keycloak SCIM URL return 404?

The route is not registered. Either the server was started without --features=scim-api, or scimApiEnabled is still false on that realm. Both have to be on. In this 26.7.4 test, that generic 404 meant the SCIM route was not available, and the same URL moved to a SCIM-formatted 401 once both switches were enabled.

Why does Keycloak SCIM return Invalid token audience?

The client-credentials token is valid, but aud does not contain the realm SCIM base URL (http://<host>:<port>/realms/<realm>/scim/v2). Add an Audience mapper with that exact custom audience on the access token, then request a new token. Tokens already issued do not pick up the mapper.

What role does a Keycloak SCIM client need?

For the read-only flow in this guide, assign view-users. It includes the query-users and query-groups permissions needed for discovery endpoints, while allowing individual user resources to be read. view-realm alone does not provide SCIM discovery access in the tested 26.7.4 setup.

What is the Keycloak SCIM base URL?

http://<host>:<port>/realms/<realm-name>/scim/v2. For the container in this guide that is http://127.0.0.1:8080/realms/golinuxcloud/scim/v2. The access token audience must match that URL exactly. Realm settings does not list a SCIM link under Endpoints in 26.7.4.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)