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

Skip to content

Conversation

@graziang
Copy link
Contributor

@graziang graziang commented Oct 21, 2025

Closes #43444

  • Created a new OAuth2GrantTypeBase implementation named JWTAuthorizationGrantProvider, to be used only with the grant type "urn:ietf:params:oauth:grant-type:jwt-bearer". This allows future support for other authorization grant types, such as SAML.

  • Introduced JWTAuthorizationGrantValidationContext, a class that provides context and centralizes JWT assertion validation. It helps avoid code duplication by having a single place for validation logic. Currently, it includes only basic checks. For example signature validation (idp side) and reuse check still to be implemented.
    It should be considered a possible refactor to reuse the logic from JWTClientValidator to reduce code duplication in JWT validation. Saving assertion state in this case is probably not needed.

  • Introduced the JWTAuthorizationGrantProvider interface with two methods:

    • isIssuer(): Used to validate the issuer based on the IDP configuration. I added this method because the issuer might be stored differently depending on the identity provider, for example, in OIDCIdentityProvider it’s stored under config node "issuer".
      Should we remove this method and standardize how issuer validation is performed across all identity providers?
    • validateAuthorizationGrantAssertion(): Takes a JWTAuthorizationGrantValidationContext to perform all JWT assertion validations (like signature) according to the IDP specification, and returns a BrokeredIdentityContext. In this PR, there is no specific validation implemented for the OIDCIdentityProvider, only a simple implementation that returns the username.
  • Added JWTAuthorizationGrantTest, using the new test suite, which includes some basic tests.

@graziang graziang marked this pull request as ready for review October 21, 2025 13:32
@graziang graziang requested a review from a team as a code owner October 21, 2025 13:32
@mposolda mposolda self-assigned this Oct 22, 2025
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.

Nice @graziang! Just the call to the AlternativeLookupProvider is really requested in my review. The other comments are suggestions or questions I have. 😄

I also remember talking about a replay check for this grant. I suppose we will manage it as a follow-up. But maybe we can create all the related issues (replay check, use username or id to lookup the user or something more complicated,...) to not forget about extra points (we can even not implement them, but we have them into consideration).

Comment on lines 136 to 144
private IdentityProviderModel getAuthorizationGrantIdentityProviderModel(KeycloakSession session, String issuer) {
return session.identityProviders().getAllStream()
.filter(idpModel -> {
IdentityProvider idp = IdentityBrokerService.getIdentityProvider(session, idpModel.getAlias());
return idp instanceof JWTAuthorizationGrantProvider authorizationGrantProvider && authorizationGrantProvider.isIssuer(issuer);
})
.findFirst()
.orElse(null);
}
Copy link
Contributor

@rmartinc rmartinc Oct 22, 2025

Choose a reason for hiding this comment

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

I think that here we need to use the same idea Stian used to locate the IdP in SPIFFE that caches the lookup. It's the AlternativeLookupProvider. Maybe with this the isIssuer method is not needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1, good catch!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I missed this provider. Removed isIssuer() and used the AlternativeLookupProvider. I had to make some adjustments to check that the found idp provider is an JWTAuthorizationGrantProvider

Comment on lines 95 to 102
//apply identity provider mappers
Set<IdentityProviderMapperModel> mappers = session.identityProviders().getMappersByAliasStream(identityProviderModel.getAlias())
.collect(Collectors.toSet());
KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory();
for (IdentityProviderMapperModel mapper : mappers) {
IdentityProviderMapper target = (IdentityProviderMapper)sessionFactory.getProviderFactory(IdentityProviderMapper.class, mapper.getIdentityProviderMapper());
target.preprocessFederatedIdentity(session, realm, mapper, brokeredIdentityContext);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe better to execute the mappers in the validateAuthorizationGrantAssertion inside the IdP?

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we need to call mappers at all? Added also inline comment related to that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, you are right. If we are just getting the user by the ID, no mapper is needed here. @graziang already removed this part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah we probably don't need mappers, we only need to find the linked user using the subject from the assertion JWT, at least for now. Removed the mapper block.


//user must exist in keycloak
FederatedIdentityModel federatedIdentityModel = new FederatedIdentityModel(identityProviderModel.getAlias(), brokeredIdentityContext.getUsername(), brokeredIdentityContext.getUsername(), brokeredIdentityContext.getToken());
UserModel user = this.session.users().getUserByFederatedIdentity(realm, federatedIdentityModel);
Copy link
Contributor

Choose a reason for hiding this comment

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

With this the subject of the token should be the user id in keycloak. Don't remember what was finally decided here. For me it's OK for this first implementation, but probably we need something more (at least I would add the username too, I remember we talked about more complicated ideas).

Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIR it was decided to go this way initially.

We may need something more just if requested by users though... In that case, we would need some additional configuration on the IDP for how to map the assertion to the Keycloak UserModel ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

mmm I have a doubt if we need just that the subject of the assertion JWT must be the username of the user in keycloak without any link to the idp, or if we need a link between the keyloak user and the idp and the subject of the assertion can be the linked external id of the keycloak user

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that the link is needed, because the caller is using the ID in the broker to identify the user, so in the JWT authorization grant realm we have only that ID (ID in the original realm) in the link. So I think that it's OK like it is now.

Copy link
Contributor

@mposolda mposolda left a comment

Choose a reason for hiding this comment

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

@graziang This is good start, Thanks!

I've added few minor comments inline, but I hope we can go with the PR.

Before merging, I suggest to create few sub-issues, which might be done as a follow-up to this (suggesting sub-issues before merging this, so we don't forget about it later).

Those are:

  • It seems there is not yet validation of the signature on JWT assertion. This will need to be added as a follow-up at least (Together with some tests for test with the invalid signature assertion etc). The validation should be probably done against JWK keys of the identity provider (provided for example from jwks_url of IDP).

  • The JWTAuthorizationGrantValidationContext seems to have some validation, which duplicates the code, which can be rather re-used from other places. Especially validation of the "token being active" etc. It can be probably good to re-use AbstractJWTClientValidator somehow .

  • Scopes processing (See the inline comment, which I've added upon your TODO). Not yet sure about the details, but just some issue as a placeholder to doublecheck what we need to do for scopes can be good.

Feel free to create additional sub-issues if you are aware of some follow-up tasks not yet being done in this PR.

}

//apply identity provider mappers
Set<IdentityProviderMapperModel> mappers = session.identityProviders().getMappersByAliasStream(identityProviderModel.getAlias())
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not sure if it makes sense to call preprocessFederatedIdentity on IdentityProviderMapper considering that we don't call any other methods of the mappers (like methods importNewUser or updateBrokeredUser ). Any reason for that? If there is no reason, I vote for remove this block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the mappers!


@Override
public BrokeredIdentityContext validateAuthorizationGrantAssertion(JWTAuthorizationGrantValidationContext context) {
if (!Profile.isFeatureEnabled(Profile.Feature.JWT_AUTHORIZATION_GRANT)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need additional feature check here? Considering that the method is invoked from JWTAuthorizationGrantType, which itself is disabled when the feature is disabled, then maybe this additional check might be redundant?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah is not so useful, removed.

throw new RuntimeException("User not found");
}

String scopeParam = formParams.getFirst(OAuth2Constants.SCOPE);
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be good to create follow-up issues for this one.

@graziang
Copy link
Contributor Author

@mposolda @rmartinc Thanks for the review! I've applied the suggestions in a separate commit and created the follow up tasks!

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.

Thank @graziang!

Copy link
Contributor

@mposolda mposolda left a comment

Choose a reason for hiding this comment

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

@graziang @rmartinc Thanks for the updates and additional feedback and review! I hope to merge once tests are OK (did re-run the tests as test failures seems unrelated to the changes in this PR).

@mposolda mposolda merged commit a25a026 into keycloak:main Oct 22, 2025
125 of 127 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implementation of OAuth2GrantType for authorization grant based on RFC 7523

3 participants