Description
Before reporting an issue
- I have read and understood the above terms for submitting issues, and I understand that my issue may be closed without action if I do not follow them.
Area
token-exchange
Describe the bug
I'm following this guide to enable token exchange between two users in Keycloak. I've also read through the official docs a few times now.
What I'm trying to do is have a back end service log in as user A
in realm A
, and then exchange that token for user B
in realm B
. Both users already exist.
Everything seems to work at first - This gets me an access token for user A
in realm A
:
KEYCLOAK_HOST="https://..."
SOURCE_REALM_NAME="TEST-source"
SOURCE_CLIENT_NAME="realm_A_login"
SOURCE_CLIENT_SECRET="..."
SOURCE_REALM_USER_NAME="user_A"
SOURCE_REALM_USER_PASSWORD="..."
SOURCE_TOKEN=$(curl -s \
-X POST \
"${KEYCLOAK_HOST}/realms/${SOURCE_REALM_NAME}/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "client_id=${SOURCE_CLIENT_NAME}" \
--data-urlencode "username=${SOURCE_REALM_USER_NAME}" \
--data-urlencode "password=${SOURCE_REALM_USER_PASSWORD}" \
-d 'grant_type=password' \
-d "client_secret=${SOURCE_CLIENT_SECRET}" \
-d 'scope=openid profile roles' \
| jq -r .access_token)
echo "SOURCE_TOKEN: ${SOURCE_TOKEN}"
Pulling apart that JWT, I get what I would expect:
{
"exp": 1742287643,
"iat": 1742244443,
"aud": "account",
"sub": "98969f6f-38b7-4bd1-b353-957b88721919",
"typ": "Bearer",
...
"scope": "openid email profile",
"email_verified": true,
"name": "User A",
"preferred_username": "user_A",
...
}
Now, exchange that for a token for user B
:
DESTINATION_REALM_NAME="TEST-Destination"
DESTINATION_CLIENT="realm_B_login"
DESTINATION_CLIENT_SECRET="..."
DESTINATION_IDP_NAME="IdP_TEST-source"
USERNAME="........-....-....-....-............"
ACCESS_TOKEN=$(curl -L
"${KEYCLOAK_HOST}/realms/${DESTINATION_REALM_NAME}/protocol/openid-connect/token" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
-d "subject_token=${SOURCE_TOKEN}" \
--data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:access_token' \
-d "client_id=${DESTINATION_CLIENT}" \
-d "client_secret=${DESTINATION_CLIENT_SECRET}" \
-d "subject_issuer=${DESTINATION_IDP_NAME}" \
-d "audience=${DESTINATION_CLIENT}" \
-d "requested_subject=${USERNAME}" \
-d 'scope=openid profile roles' | jq -r .access_token)
echo "ACCESS_TOKEN: ${ACCESS_TOKEN}"
That seems great - I get an access token back. However, inspecting the token, I seem to have the same user A
details:
{
"exp": 1742287643,
"iat": 1742244443,
"jti": "5cf7a005-2c2f-464c-868e-3219c344b543",
"aud": [
"account",
"realm_B_login"
],
"sub": "a91794b9-f566-4162-abf7-544e3127ab70",
"typ": "Bearer",
"azp": "realm_B_login",
"sid": "c7ffe470-0024-42d0-bbad-792ee092d2d3",
...
"scope": "openid email profile",
"email_verified": false,
"name": "User A",
"preferred_username": "user_a@my.company",
...
}
So - the audience is a little different, and its using email for a username, but the big issue is its for the user A
, not user B
.
Pulling up the Keycloak console - lo and behold it created a copy of user A
in realm B
- and that's what its giving me a token for.
What am I missing here? I'm not asking it to create a new user in my destination realm, and I'm clearly saying my requested_subject
is user B
. Why is it creating a copy of user A
, and why isn't it giving me an access token for user B
?
Version
Keycloak 25.0.1
Regression
- The issue is a regression
Expected behavior
I would expect the token-exchange to mint me an access token for the existing user B
, and not create a new user A
in the destination realm.
Actual behavior
User A gets duplicated into realm B, and I get an access token minted for that new shadow user.
How to Reproduce?
on server - enable fine grained permissions and token exchange / feature preview
create realm A
create realm B
create user A in realm A, set password
with realm A:
- create service login client, providing direct access
- create token_exchange client (will be used for IdP sso exchange)
with realm B:
- create realm B login client
- create realm A IdP
- use well-known/configuration endpoint from realm A
- specify client/secret for token_exchange from realm A
- enable fine-grained permissions
- create a new token-exchange client policy, add the IdP token-exchange permission to the policy
- create user B
Run the bash snippets above to obtain token for user A in realm A, and then exchange for token for user B in realm B.
Anything else?
No response