-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Experimental feature for JWT Authorization Grant #43624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Closes keycloak#43444 Signed-off-by: Giuseppe Graziano <[email protected]>
rmartinc
left a comment
There was a problem hiding this 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).
| 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); | ||
| } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, good catch!
There was a problem hiding this comment.
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
| //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); | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 ...
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
mposolda
left a comment
There was a problem hiding this 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_urlof IDP). -
The
JWTAuthorizationGrantValidationContextseems 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-useAbstractJWTClientValidatorsomehow . -
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()) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
Closes keycloak#43444 Signed-off-by: Giuseppe Graziano <[email protected]>
rmartinc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank @graziang!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Closes #43444
Created a new
OAuth2GrantTypeBaseimplementation namedJWTAuthorizationGrantProvider, 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
JWTClientValidatorto reduce code duplication in JWT validation. Saving assertion state in this case is probably not needed.Introduced the
JWTAuthorizationGrantProviderinterface 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, inOIDCIdentityProviderit’s stored under config node "issuer".Should we remove this method and standardize how issuer validation is performed across all identity providers?
validateAuthorizationGrantAssertion(): Takes aJWTAuthorizationGrantValidationContextto perform all JWT assertion validations (like signature) according to the IDP specification, and returns aBrokeredIdentityContext. In this PR, there is no specific validation implemented for theOIDCIdentityProvider, only a simple implementation that returns the username.Added
JWTAuthorizationGrantTest, using the new test suite, which includes some basic tests.