8000 Admin REST API returns 401 for tokens issued by the managed realm (service account with realm-management roles); master-realm tokens still work · Issue #54748 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Admin REST API returns 401 for tokens issued by the managed realm (service account with realm-management roles); master-realm tokens still work #54748

Closed

Description

@selbiselbi

Before reporting an issue

  • I have read and understood the above terms for submitting issues, and I understand that my report will be closed without further comment if it does not meet them.

Area

admin/api

Describe the bug

After upgrading from 26.8.1 to 26.8.2, every call to the Admin REST API made with an access token issued by the realm being administered returns 401 {"error":"HTTP 401 Unauthorized"}, even though the token is valid and carries the required realm-management client roles.

The same call made with a token issued by the master realm succeeds (200). Downgrading to 26.8.1 (same database, same configuration) makes the realm-issued token work again.

Our use case is the standard one for backend user management: a confidential client in realm myrealm, service account enabled, service account granted realm-management roles (view-users, query-users, manage-users, view-clients), token obtained with client_credentials, then GET /admin/realms/myrealm/users.

Decoded token (values anonymized), as sent to the Admin API:

{
  "iss": "https://keycloak.example.com/realms/myrealm",
  "typ": "Bearer",
  "azp": "admin-client",
  "sid": null,
  "iat": 1793955941,
  "exp": 1793956541,
  "resource_access": {
    "realm-management": {
      "roles": ["manage-users", "view-users", "view-clients", "query-clients", "query-groups", "query-users"]
    }
  }
}

The token was used 4 seconds after issuance (not expired), and iss matches the configured hostname.

What we ruled out on 26.8.2:

Variation Result
client_credentials token from myrealm, client with Full scope allowed ON (default) 401
Same, Full scope allowed OFF, realm-management roles mapped to the client scope through a composite realm role (token contents shown above, account roles gone) 401
Same, plus Use refresh tokens for client credentials grant ON (token now has a sid, i.e. backed by a real user session) 401
Password-grant token from the master realm (admin-cli, user with admin rights), same request on /admin/realms/myrealm/users 200
Any of the myrealm tokens above against 26.8.1 200

So the rejection does not depend on scope, roles, or the presence of a user session: it seems to depend only on the token being issued by a realm other than master.

Possibly related: 26.8.2 ships fixes in the realm scoping of the Admin REST API (e.g. CVE-2026-94215). This is only a guess on our side.

Since 26.8.2, the token endpoint also logs this warning for the client. Disabling Full scope allowed (second row of the table) did not change the 401:

WARN [org.keycloak.protocol.oidc.endpoints.TokenEndpoint.full-scope-allowed] Client 'admin-client' in the realm 'myrealm' has 'Full scope allowed' switch enabled. This client switch is deprecated and will be removed in the future. [...]

Version

26.8.2 (last working version: 26.8.1)

Regression

  • The issue is a regression

Expected behavior

A token issued by realm myrealm to a service account holding realm-management roles view-users / query-users can call GET /admin/realms/myrealm/users, as it does in 26.8.1 and as documented for realm-level administration with service accounts.

Actual behavior

401 Unauthorized with body {"error":"HTTP 401 Unauthorized"} and no WWW-Authenticate header.

How to Reproduce?

Self-contained script using the official images (quay.io/keycloak/keycloak:<version>, start-dev, nothing else). It creates a realm myrealm with a confidential client admin-client whose service account gets the realm-management roles view-users and query-users, then calls GET /admin/realms/myrealm/users once with a client_credentials token from myrealm and once with a master-realm token.

./repro.sh 26.8.1 18081
./repro.sh 26.8.2 18082
repro.sh
#!/usr/bin/env bash
# Reproduces the Keycloak 26.8.2 regression: the Admin REST API rejects (401) a token issued by the
# realm being administered, while a token issued by the master realm is accepted (200).
#
# Usage:   ./repro.sh <keycloak-version> <host-port>
# Example: ./repro.sh 26.8.1 18081
#          ./repro.sh 26.8.2 18082
#
# Runs the official image quay.io/keycloak/keycloak:<version> in start-dev mode (no Java needed on
# the host). Expected duration: 1 to 2 minutes per version, plus the image pull on first run.
# Requirements: docker, curl, jq.
set -u

VERSION="${1:?missing Keycloak version (e.g. 26.8.2)}"
PORT="${2:?missing host port (e.g. 18082)}"
IMAGE="quay.io/keycloak/keycloak:$VERSION"
NAME="kc-repro-$PORT"
BASE="http://localhost:$PORT"
START_TIMEOUT=180   # seconds
T0=$(date +%s)

log() { printf '[%s +%3ss] %s\n' "$(date +%H:%M:%S)" "$(( $(date +%s) - T0 ))" "$*"; }

STARTED=""
fail() {
  log "FAILED: $*"
  if [ -n "$STARTED" ]; then
    log "Last lines of the Keycloak log (docker logs $NAME):"
    docker logs --tail 20 "$NAME" 2>&1 | sed 's/^/    /'
  fi
  exit 1
}

cleanup() {
  if [ -n "$STARTED" ]; then
    log "Removing container $NAME..."
    docker rm -f "$NAME" >/dev/null 2>&1
    log "Container removed."
  fi
}
trap cleanup EXIT

for cmd in docker curl jq; do command -v "$cmd" >/dev/null || fail "'$cmd' is not installed"; done
docker info >/dev/null 2>&1 || fail "cannot talk to the Docker daemon (is it running? is this user in the docker group?)"
if docker ps -a --format '{{.Names}}' | grep -qx "$NAME"; then
  fail "a container named $NAME already exists (leftover from a previous run?): docker rm -f $NAME"
fi

# ── 1. Image ─────────────────────────────────────────────────────────────────────────────
log "1/6 Pulling $IMAGE (only slow on the first run)..."
docker pull -q "$IMAGE" >/dev/null || fail "cannot pull $IMAGE"

# ── 2. Start ─────────────────────────────────────────────────────────────────────────────
log "2/6 Starting Keycloak $VERSION in start-dev on $BASE (container $NAME)..."
docker run -d --name "$NAME" -p "127.0.0.1:$PORT:8080" \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  "$IMAGE" start-dev >/dev/null || fail "docker run failed (port $PORT already in use?)"
STARTED=1

while :; do
  code=$(curl -s --noproxy localhost -o /dev/null -w "%{http_code}" "$BASE/realms/master" 2>/dev/null)
  [ "$code" = "200" ] && break
  [ "$(docker inspect -f '{{.State.Running}}' "$NAME" 2>/dev/null)" = "true" ] || fail "Keycloak stopped during startup"
  elapsed=$(( $(date +%s) - T0 ))
  [ "$elapsed" -ge "$START_TIMEOUT" ] && fail "Keycloak still not answering after ${START_TIMEOUT}s"
  [ $(( elapsed % 10 )) -eq 0 ] && log "    waiting for Keycloak... (last log line: $(docker logs --tail 1 "$NAME" 2>&1 | cut -c1-100))"
  sleep 1
done
log "    Keycloak is up: $(docker logs "$NAME" 2>&1 | grep -o 'Keycloak [0-9.]* on JVM' | head -1)"

# ── 3. Setup, with kcadm inside the container ────────────────────────────────────────────
kcadm() { docker exec "$NAME" /opt/keycloak/bin/kcadm.sh "$@" --config /tmp/kcadm.config; }

log "3/6 Logging kcadm into the master realm..."
kcadm config credentials --server http://localhost:8080 --realm master --user admin --password admin \
  || fail "kcadm login"

log "4/6 Creating realm myrealm, confidential client admin-client and its realm-management roles..."
kcadm create realms -s realm=myrealm -s enabled=true || fail "realm creation"
kcadm create clients -r myrealm -s clientId=admin-client -s publicClient=false -s serviceAccountsEnabled=true \
   -s standardFlowEnabled=false -s secret=s3cret || fail "client creation"
kcadm add-roles -r myrealm --uusername service-account-admin-client --cclientid realm-management \
   --rolename view-users --rolename query-users || fail "role assignment"

# ── 4. Token issued by the administered realm ────────────────────────────────â”
8000
€───────────
log "5/6 client_credentials token from myrealm, then GET /admin/realms/myrealm/users..."
TOKEN=$(curl -s --noproxy localhost "$BASE/realms/myrealm/protocol/openid-connect/token" \
   -d grant_type=client_credentials -d client_id=admin-client -d client_secret=s3cret | jq -r .access_token)
[ -n "$TOKEN" ] && [ "$TOKEN" != "null" ] || fail "no token for admin-client"
log "    realm-management roles in the token: $(echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null | jq -c '.resource_access["realm-management"].roles' 2>/dev/null)"
MYREALM=$(curl -s --noproxy localhost -o /dev/null -w '%{http_code}' "$BASE/admin/realms/myrealm/users?max=1" \
   -H "Authorization: Bearer $TOKEN")

# ── 5. Token issued by the master realm ──────────────────────────────────────────────────
log "6/6 Master realm token (admin-cli), then the same request..."
MTOKEN=$(curl -s --noproxy localhost "$BASE/realms/master/protocol/openid-connect/token" \
   -d grant_type=password -d client_id=admin-cli -d username=admin -d password=admin | jq -r .access_token)
[ -n "$MTOKEN" ] && [ "$MTOKEN" != "null" ] || fail "no master token"
MASTER=$(curl -s --noproxy localhost -o /dev/null -w '%{http_code}' "$BASE/admin/realms/myrealm/users?max=1" \
   -H "Authorization: Bearer $MTOKEN")

# ── Result ───────────────────────────────────────────────────────────────────────────────
log "Result for Keycloak $VERSION:"
echo "    myrealm token -> $MYREALM"
echo "    master token  -> $MASTER"
if [ "$MYREALM" = "401" ] && [ "$MASTER" = "200" ]; then
  log "=> Regression reproduced."
elif [ "$MYREALM" = "200" ] && [ "$MASTER" = "200" ]; then
  log "=> No regression on this version."
else
  log "=> Unexpected result. Last lines of the Keycloak log:"
  docker logs --tail 30 "$NAME" 2>&1 | sed 's/^/    /'
fi

Result (last lines of each run):

# 26.8.1
    myrealm token -> 200
    master token  -> 200
=> No regression on this version.

# 26.8.2
    myrealm token -> 401
    master token  -> 200
=> Regression reproduced.

Anything else?

Our production setup, in case it matters (the reproduction script above does not use any of it):

  • Official image quay.io/keycloak/keycloak:26.8.2, start (not optimized), PostgreSQL 17.
  • Behind Traefik v2 (TLS termination): KC_HOSTNAME=https://keycloak.example.com, KC_PROXY_HEADERS=xforwarded, KC_HTTP_ENABLED=true, KC_HOSTNAME_STRICT=false. The deprecated KC_PROXY=edge and KC_PROXY_ADDRESS_FORWARDING=true are also still set.
  • Switched in place 26.8.1 → 26.8.2 → 26.8.1 → 26.8.2 → 26.8.1 on the same database: realm-issued admin tokens fail on every 26.8.2 run and work on every 26.8.1 run. We are back on 26.8.1 for now.
  • Workaround: stay on 26.8.1, or obtain the admin token from the master realm.

Activity

  1. added theissue type on Nov 6, 2026
  2. maxkre96 commented on Nov 6, 2026

    @maxkre96

    We have verified this issue in our environment. Also rolled back to 26.8.1. However, this prevents us from upgrading our clusters, leaving them vulnerable to CVE-2026-111497.

  3. temp043 commented on Nov 6, 2026

    @temp043

    We can also verify this. In our case, our automated keycloak installer (with a fresh 26.8.2) fails, since it is unable to call GET /admin/realms with a freshly generated token. The user behind the token actually has admin role, including all manage-realm permissions.

  4. jomu78 commented on Nov 6, 2026

    @jomu78

    I can confirm also

  5. angry-botanist commented on Nov 6, 2026

    @angry-botanist

    Confirmed issue with OpenTalk and the [user_search] functionality, performed a rollback to 26.8.1 for now.

  6. Angelos3030 commented on Nov 6, 2026

    @Angelos3030

    Confirmed

  7. 0xM58 commented on Nov 6, 2026

    @0xM58

    Confirmed
    Since this release is fixing 2 critical, could you please prioritize this so we don't rollback

  8. n3vu0r commented on Nov 6, 2026

    @n3vu0r

    A temporary fix is to add spi-admin--audience-check-enabled=false to the configuration file.

    A permanent fix is to add the kc-admin-api scope to the service account's defaultClientScopes.

  9. batorse commented on Nov 6, 2026

    @batorse

    More information on the client-scope "kc-admin-api" can be found in the migration guide for 26.8.2.

  10. selbiselbi commented on Nov 6, 2026

    @selbiselbi
    Author

    Should I close this issue then ? Seems like I didn't read carefully enough, my bad.

  11. batorse commented on Nov 6, 2026

    @batorse

    Looks like it is an intended change and not a bug.

    On the bight side this issue is a clear description of the problem as well as the solution and will help others with the same problem. It is better understandable then the very close up technical and confusingly formatted release notes. So thanks for writing this up and to n3vu0r for providing the solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/admin/apiAdmin REST API (v1) — managing realms, users, clients, roles, groupskind/bugCategorizes a PR related to a bugteam/core-shared

    Type

    Fields

    No fields configured for bug.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    0