Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@mposolda
Copy link
Contributor

@mposolda mposolda commented Feb 3, 2025

closes #35505

The summary of most important changes:

  • When creating token, it is using clientSession of "requester" client for the token-exchange instead of the "target" client . This means also expiration and other token settings are taken from "requester" (The target client won't work due with multiple audiences there may not be single target client. Using "requester" is in general more clean IMO and there are more reasons for it (those are specified in the google doc mentioned below)

  • The client-scopes applied are based on the available scopes of the "requester" client as well. This makes token-exchange consistent with other grant types in Keycloak where clientSession is always based on the client, which triggered the grant request

  • The parameter audience is used to filter audiences from the token, which are not present in the audience parameter (if parameter is included).

  • For now, changes are applied just for standard token exchange when requested_token_type is refresh-token or access-token. This PR does not yet cover scenario when SAML2 assertion is requested_token_type . SAML token-exchange still uses assertion based on the audience client rather than requester client. Updating SAML is a possible follow-up.

I've added the 4 commits, but first 2 commits are just refactoring without no real changes in the behaviour (dealing with abstract methods to make it easier to update V2 without too much code duplications etc). The "real changes" are especially in the commit 1878af1 . I've did the separate commits, so the "real changes" can be easily reviewed. Planning to squash before merge.

Motivation

Added basic motivation for this here: https://docs.google.com/document/d/16Ug7tHDq3EAZHv0PaBVe4OQ4poTDHbaXoGbkXQtO2iI/edit?tab=t.0#heading=h.voxwt2ychgno

Added the test ClientTokenExchangeAudienceAndScopesTest , which tests the same scenario outlined in the "Example" inside that google document.

The token-exchange V1 should have same behaviour as before. The only updated is standard token-exchange V2. The test for V1 is also unchanged, but there is some refactoring in the test to make it possible to re-use the same test for V2 with some abstract methods overriden (not sure if it is cleaner to rather have dedicated test classes for V1 and V2 with all the test methods (as there will be more differences) and avoid AbstractStandardTokenExchangeTest ? Feedback welcome...

@mposolda mposolda self-assigned this Feb 3, 2025
@mposolda mposolda force-pushed the 35505-token-exchange-more-audiences-requester-client branch from 69ff60a to bf9e594 Compare February 4, 2025 10:41
@mposolda mposolda changed the title 35505 token exchange more audiences requester client 35505 token exchange more audiences - requester client used for clientSession and scopes Feb 4, 2025
@mposolda mposolda marked this pull request as ready for review February 5, 2025 07:20
@mposolda mposolda requested review from a team as code owners February 5, 2025 07:20
Copy link
Contributor

@rmartinc rmartinc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been checking this and it worked in the simple tests I had been using before for previous issues. So I did not detect any problem because now the requester is the one generating the token. In general the PR is aligned with the proposed document. So let's see what other people comment about the change.

return exchangeClientToOIDCClient(targetUser, targetUserSession, requestedTokenType, targetAudienceClients, scope);
case OAuth2Constants.SAML2_TOKEN_TYPE:
return exchangeClientToSAML2Client(targetUser, targetUserSession, requestedTokenType, targetClient);
return exchangeClientToSAML2Client(targetUser, targetUserSession, requestedTokenType, targetAudienceClients);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have checked and right now the OIDC to SAML also works as before with standard V2, it doesn't matter the client set in the session context is the requester one. Just pointing this out.

Copy link
Contributor Author

@mposolda mposolda Feb 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in this PR I've updated just OIDC token-exchange, but I did not yet updated SAML token-exchange. SAML still works same as before (using first audience value and creating clientSession for it). Updated PR description about it as well.

I suppose that for SAML, we may want to remove the ability of token-exchange as it would be strange to support it with the "target" client when OAuth is doing "requester" client? But maybe that is for further discussions/PRs? For now I've ignored SAML and kept the same behaviour as before.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, exactly, I just was curious about what was going to happen now.

Copy link
Contributor

@graziang graziang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mposolda Thanks, added a couple of comments.


// The "target-client3" is valid client, but unavailable to the user. Request allowed, but "target-client3" audience will not be available
OAuthClient.AccessTokenResponse response = oauth.doTokenExchange(TEST, accessToken, List.of("target-client1", "target-client3"), "requester-client", "secret", null);
assertAudiencesAndScopes(response, List.of("target-client1"), List.of("default-scope1", "optional-scope2"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should optional-scope2 be present in the exchanged token? Since we are filtering by audience target-client1 and optional-scope2 is mapped to target-client2-role, which is a client role of target-client2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Unfortunately what we currently have is:

  • A way to add aud claim to the token based on the scopes requested (Either by scope parameter or by default client scopes)
  • A way to filter audiences after client-scopes / protocol-mappers processing is finished

But we don't have a good way for the use-case like: "Give me the scope, which added audience foo to the token" .
We may have some indirect ways to do it, but they seem to be a fragile ATM (Considering that aud claim could be added to the token by various ways like client-roles or by AudienceProtocolMapper or by any custom protocol mapper implementation). So ATM we may need to include the optional-scope2 still IMO. But I think the discussion we had about possible follow-ups (like having the "Consumed scopes" available on the "Resource clients" etc) should be a way to properly address this and eventually make sure that token would contain just the scopes needed for the requested audience.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea was: "Don't include scopes in the exchanged token that are linked to client roles belonging to a client that is omitted in the aud request parameter."

I was thinking of using something similar to the DefaultClientSessionContext.isClientScopePermittedForUser() method and then filtering the scopes based on the client roles that only belong to the clients requested in the exchange request via the aud claim. I'm not sure if you were thinking of something similar.

It doesn't seem correct to me to have the optional-scope2 scope in the token because the user has the target-client2-role but target-client2-role is not in resource-access.
But it's fine to ignore this for now and address it later as a follow-up with the "Consumed scopes."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Thanks! I can think something like this might work even though there might be corner-cases related to this. It will probably need some more though and refactoring (maybe some changes in DefaultClientSessionContext as well as IMO we would need to know the client-scopes in advance before we process them to make this working).

Is it ok for you to go with this PR as is and then handle this as a follow-up?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, thanks!


List<String> audiencesToRemove = new ArrayList<>(newTokenAudiences);
for (String audienceParam : params.getAudience()) {
boolean removed = audiencesToRemove.remove(audienceParam);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on just ignoring the audiences that are not available, in that case maybe we can simplify a bit the method using List.retainAll()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but on the other hand, with scopes, we block the auth request with "Invalid scope" when a not allowed scope is included, so I'm not sure...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. For scopes we block the auth-request with "Invalid scope" . So far, the discussion was more towards to "reject" the request in case that requested audience is unavailable.

In this PR, I've used the "ignore" approach for now, but I've created a follow-up task for update this to "reject" #37104 (See also the example and question referenced from that issue from the Google doc). There would be some more updates in the test needed, so I did not yet used the "reject" approach in this PR.

WDYT? Does it work for you to go with "ignore" in this PR and then have follow-up for "reject" approach?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, okay with ignoring it in this PR and handling it in another task

Copy link
Contributor

@graziang graziang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mposolda thanks!

@mposolda mposolda merged commit 2289ed9 into keycloak:main Feb 6, 2025
77 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for multiple values of audience

3 participants