keycloak: Token Exchange setting incorrect clientId
Describe the bug
I’m attempting to do an internal-to-internal token exchange. The exchange returns successfully, but it appears that the issuedFor
or azp
claim is set to the incorrect client.
This poses a problem in that the exchanged token is not able to be refreshed with its access token because the refresh token is bound to the wrong client.
I’m exchanging the token so that I can get the scopes, etc from the target client. That works great here, but I then need to be able to use the refresh token to exchange it for a new access token that has those same scopes. In the current state, I instead have to re-initiate the token exchange all over again.
I’m I understanding token exchange correctly here? I took it that you take a token from client A and are able to completely exchange it for a token from client B. The way this is currently set up, the token is exchanged for another client A token, but that has been enriched with scopes from client B. Also, why is no identity token returned from the exchange?
Version
15.0.2
Expected behavior
The Access Token and Refresh token should have the azp
set to the target client.
Actual behavior
The Access Token and Refresh token are set to the starting client.
How to Reproduce?
To reproduce, simply follow the internal-to-internal token exchange docs and inspect the tokens that are returned. The azp
will be set to the starting client.
Once the tokens have been exchanged, attempt to issue a refresh against the target-client.
POST /realms/:realm/protocol/openid-connect/token
grant_type: refresh_token
refresh_token: <token>
client_id: target-client
Response:
{
"error": "invalid_grant",
"error_description": "Invalid refresh token. Token client and authorized client don't match"
}
This makes sense given the azp
is for the starting-client, not the target.
Anything else?
About this issue
- Original URL
- State: closed
- Created 3 years ago
- Reactions: 12
- Comments: 32 (9 by maintainers)
This is what confuses me a lot. What is the sense in being able to request a refresh token (via
requested_token_type
:urn:ietf:params:oauth:token-type:refresh_token
), but not being able to refresh the token?Allowing public clients to exchange tokens opens security holes and that is the reason behind the last updates we did in this area. Conceptually, it is also wrong because a public client is conceptually a client and not a resource server.
You might consider looking at some SPA architectures in this specification. In particular, the first one is related to both SPA and API living within the same domain.
Another possible approach is to provide your own custom
TokenExchangeProvider
implementation so that you can support your legacy system as an additional subject token type.Preface: I no longer work at the company where I was utilizing this technology.
But the setup was such that we had two clients that we would authenticate clients through. The first (or starting) client was used to do the initial authentication. Then we would perform a token swap to the second client based on the company account the user was operating under. This would swap out the token and give them more specific scopes that was appropriate for the account they were acting as.
I assumed the token exchange was a true exchange such that it was as if the user fully authenticated through the target client, not just swapped.
But it seems to me that the swapped token is still bound in some way to the original authenticated client. This makes the refresh logic a bit more complex because the original token doesn’t become throw away such that we have to continually issue another swap once the swapped token expires.
I just came across this Keycloak issue and it seems related: KEYCLOAK-19116
After putting in quite some effort into setting up the token exchange logic, so an admin user could impersonate another user. And working through the hurdles of the fact it is also still semi in preview, I also hit upon this issue. The flow from within Keycloak sadly was and is too difficult for the support staff of the project. And this seemed like a nice, sensible way to go about it. First, I request the API server to exchange the token for the user and then refresh the client with the newly received tokens using the
Keycloak.init()
method. This sadly always results in:<<keycloak url>>/realms/<<realm>>/protocol/openid-connect/token
400 bad requestInvalid refresh token. Token client and authorized client don't match
Meaning, like most others here, I would have to build a BFF-proxy just for one piece of functionality that I would be more than comfortable configuring through a setting. Allowing the confidential client to exchange the token to the public client with which the users on the frontend login. With the correct
AUZ
update:
For now at least till the end of the year we opted to overwrite the TokenExchangeProvider based on the DefaultTokenExchangeProvider. And modify the AUZ directly within it by changing lines:
responseBuilder.accessToken.issuedFor(targetClient.clientId)
And
responseBuilder.refreshToken.issuedFor(targetClient.clientId)
Within the
exchangeClientToOIDCClient
methodSimilar problem here. 😕
What I want to do is to use a confidential client to create a new user via POST request to the
/admin/realms/{realm}/users
endpoint, and then use the token exchange feature to acquire an access and refresh token for this newly created user, but bound to another, public client instead (as these tokens are intended to be used in our frontend).With Keycloak 17 Legacy, I got it to work using the method described by @moribvndvs, namely putting the target client_id into both the
audience
andclient_id
fields and removing theclient_secret
field. This way I received a token that has both the source and target client IDs in itsaud
property and the target client ID in itsazp
property.After upgrading to Keycloak 18 however, this workaround does not work anymore and the call to the token exchange endpoint results in
access_denied: Client is not the holder of the token
. Without the workaround (with having the source client ID in theclient_id
field and adding theclient_secret
field again, just as described in the documentation for the token exchange), it’s back to the original behavior, having both the source and target client IDs in itsaud
property, but the source client ID inazp
. When I try to use this token with the target client, for example to refresh it on the frontend side, I get the same error @EmadiGhobad described, namelyInvalid refresh token. Token client and authorized client don't match
.Now I ask myself: Is that, what I want to do, even intended to be possible? 🤔 Means: Is it a bug in Keycloak, or is it just not possible to use a confidential client to acquire a token for another, public client?
There isn’t. That’s the conclusion I reached after two days of trialing potential alternatives.
So today I had to set up a maven project, extend default spi (annoying since you can’t access lots of private properties). Build and deploy jar - with default as ID (docker build spi override refused to swap to custom id).
It now needs rebuild and testing after every kc update.
There is also a third line that needs a fix - for refresh token it looks at source client if it’s allowed, not target.
I am trying to use the external to internal token exchange to import an user from an external idp and then use the generated token in a web application using the JS adapter.
When I try to init the javascript adapter with the generated token I encounter the same issue with the refresh token.
What would be the correct way to implement my use case?
For now as a workaround I am considering implementing a custom TokenExchangeProvider SPI that sets the azp to the targetClient, but I would prefer a more robust and maintenable solution.
I think the most common use case is simply one of conferring identity … I mean moving from confidential to public that is all I would assume … for that scenario you are only protecting what you are comfortable protecting via a very public contract and so simply being able to steer the audience correctly while providing a access refresh and id token … that probably solves a majority of the use cases at least from what I can glean from the comments here.
Even if it requires a second token exchange using the public client via the access token that spans from the confidential to public client that would probably be acceptable to most.
Private Client -> [public client azp access_token] -> public client -> [properly based access refresh and (id) ]
? I mean the two problems that were blockers were the azp not including the destination client when setting the audience and then the fact that the refresh token pointed to the original client … so the above scenario … while it invokes an extra non requested subject exchange … still keeps things sane ?
@NeoVG It does not make sense to exchange a token from a confidential client for a public client.
BTW, we found a workaround, at least kind of:
Instead of initiating the token exchange with an access token belonging to a service account of the confidential client (that was acquired using client id and secret), I’m now using the public client to acquire an access token for an actual user (the “superadmin” 😉), who has the permission to impersonate every other user in the realm (using its username and password to login), and use that one to initiate the token exchange. Since now the source and target clients are the same, it works as intended.
Problems with this solution are 1) that the public client needs to have direct access grants enabled (which might not be an option for everyone) and 2) that this “superadmin” user might be a security issue.
Same issue here. I have also noted the
azp
is for the starting client rather than the target-client. If, in the refresh call, I reference the expected target-client as the client_id, I get the same error. If I try to use the starting client instead (to match theazp
), I get a different error:However, as @nickzelei pointed out, I do seem to be able to get this to work if I change around my token exchange workflow.
First, I am using client_credential token for starting-client and exchanging it for a specific user’s token for target app:
This seems to fix the AZP, and now I can use the refresh token from that response:
So yeah, I dunno if we’ve run into a bug or the documentation needs updated. However, this appears to be working well for me for now. /shrug