Worded this question the best I could...
I'm following this guide to enable token exchange between two users in Keycloak.
The idea here is to log in as userA in realmA, and then exchange that access token for userB in realmB. Both users already exist.
Everything seems to work - 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, we 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": "[email protected]",
...
So - the audience is a little different, and its using email for a username, but 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? Reading through the official docs I don't think this is what's supposed to happen... 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?