feat(spring): add opt-in Keycloak role mapping to VaadinSecurityConfigurer - #25627
Open
totally-not-ai[bot] wants to merge 6 commits into
Open
feat(spring): add opt-in Keycloak role mapping to VaadinSecurityConfigurer#25627totally-not-ai[bot] wants to merge 6 commits into
totally-not-ai[bot] wants to merge 6 commits into
Conversation
Captures the intended contract for porting the SSO Kit Keycloak role mapper into vaadin-spring: realm and client roles become prefixed role authorities, other clients' roles are ignored, the role prefix is configurable, and an access token that is not a decodable JWT degrades to a user without role authorities instead of failing the login. The test does not compile yet - KeycloakOidcUserMapper is added once the opt-in mechanism is agreed on.
…gurer Keycloak carries realm and client roles in the access token, so they are not part of the OidcUser that OidcUserService builds and role-based access control does not see them. KeycloakOidcUserMapper, ported from SSO Kit, decodes the access token and maps those roles, and VaadinSecurityConfigurer.keycloakRoleMapping() opts in to it for a single security filter chain. Compared to the SSO Kit version, the mapper reuses the JWT decoder of a client registration instead of building one per login, takes the role prefix from VaadinRolePrefixHolder instead of hardcoding ROLE_, and maps a user without roles rather than failing the login when the access token is not a decodable JWT.
The mapper built DefaultOidcUser without the OidcUserInfo when the client registration configures a user-name attribute, which is the normal Keycloak setup. That dropped every userinfo claim from the authenticated user, and failed the login outright when the name attribute is only in the userinfo response and not in the ID token. Also resolve the role prefix when a user is mapped rather than while the filter chain is being built, since a prefix that comes from the chain itself is only known to VaadinRolePrefixHolder after configure() has run.
Artur-
reviewed
Sep 10, 2026
| "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", | ||
| "cpu": [ | ||
| "arm" | ||
| ], |
Member
There was a problem hiding this comment.
This file should not change in this PR
totally-not-ai Bot
added a commit
to vaadin/docs
that referenced
this pull request
Sep 10, 2026
…ions vaadin/flow#25625 adds UidlExpiredSessionStrategy and makes VaadinSecurityConfigurer install it by default, and vaadin/flow#25627 adds KeycloakOidcUserMapper behind a keycloakRoleMapping() opt-in. Both were ported from SSO Kit, so two of the migration gaps close. Moves the two features out of the gaps section and into the migration steps that need them, with a since badge for the version they arrive in and the previous manual approach kept in a note for earlier versions. Updates the feature mapping table and the checklist to match.
Referring to OidcUserService from VaadinSecurityConfigurer broke every application that configures Vaadin security without the optional spring-security-oauth2-client dependency: a class is verified as a whole when it is loaded, and proving that an OidcUserService may be passed as an OAuth2UserService made the verifier load types that were not there, so building the filter chain failed with a NoClassDefFoundError. Move the wiring to KeycloakRoleMapping, which is only loaded once an application asks for Keycloak role mapping and therefore has the dependency. The new test loads the configurer through a class loader that hides the dependency, so the same mistake fails in the unit tests instead of in the Spring Security integration tests.
|
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Keycloak sends user roles in the access token, so Vaadin's role-based access control does not see them. This change adds
VaadinSecurityConfigurer.keycloakRoleMapping(), which turns Keycloak realm and client roles into Spring Security role authorities for one security filter chain.What changed
KeycloakOidcUserMapper(new, ported from SSO Kit): decodes the access token and maps therealm_accessroles and theresource_accessroles of the current client id to prefixed role authorities. Roles that belong to other clients are ignored. Access-token scopes becomeSCOPE_authorities.VaadinSecurityConfigurer.keycloakRoleMapping()(new): opts in to the mapper. It only works together withoauth2LoginPage(...); without a login page the configurer logs a warning and does nothing. If the application shares its ownOidcUserServicethroughHttpSecurity, that instance is reused instead of a new one.KeycloakRoleMapping(new, package-private): holds the wiring toOidcUserService.spring-security-oauth2-clientis an optional dependency, and the JVM verifies a class as a whole when it loads it, so naming those types insideVaadinSecurityConfigurerwould break every application that does not have the dependency. Keeping them in a separate class means it is only loaded when the application asks for Keycloak role mapping.Differences from the SSO Kit version:
VaadinRolePrefixHolderinstead of a hardcodedROLE_, and is resolved when a user is mapped, because the prefix of the filter chain is only known afterconfigure()has run.OidcUserInfois always kept on the resulting user, so userinfo claims are not lost when the client registration sets a user-name attribute.Use case
An application logs users in with Keycloak, and admins are marked by a Keycloak realm role called
admin. The developer wants@RolesAllowed("admin")on a view to just work, without writing a customOidcUserService.API Changes
com.vaadin.flow.spring.security.KeycloakOidcUserMapper
com.vaadin.flow.spring.security.VaadinSecurityConfigurer
Test summary
SCOPE_authorities, and anOidcUserAuthorityis still presentresource_accessgrants for other clients produce no role authority@RolesAllowedcheckOidcUserInfoand userinfo claims, andgetName()resolveskeycloakRoleMapping()with an OAuth2 login page installs the mapper into the chain'sOidcUserServicekeycloakRoleMapping()without an OAuth2 login page configures noOidcUserServiceOidcUserServiceshared onHttpSecurityis reused, not replacedROLE_VaadinSecurityConfigurerloads and verifies through a class loader that hidesspring-security-oauth2-clientKeycloakOidcUserMapperTest.convert_realmAndClientRolesAndScopesMapped— 1KeycloakOidcUserMapperTest.convert_otherClientRolesAndMissingClaimsIgnored— 2KeycloakOidcUserMapperTest.convert_customRolePrefixAppliedToRolesOnly— 3KeycloakOidcUserMapperTest.convert_accessTokenNotAJwt_mappedWithoutRoles— 4KeycloakOidcUserMapperTest.convert_userNameAttributeUsedAsName_userInfoRetained— 5KeycloakOidcUserMapperTest.convert_decoderCreatedOncePerClientRegistration— 6VaadinSecurityConfigurerTest.keycloakRoleMapping_withOAuth2LoginPage_oidcUserServiceMapsRoles— 7VaadinSecurityConfigurerTest.keycloakRoleMapping_withoutOAuth2LoginPage_notConfigured— 8VaadinSecurityConfigurerTest.keycloakRoleMapping_sharedOidcUserService_isReused— 9VaadinSecurityConfigurerTest.keycloakRoleMapping_rolePrefixOfChain_isUsedForRoles— 10VaadinSecurityConfigurerTest.withoutOAuth2ClientOnClasspath_configurerStillLinks— 11Deliberately untested: the real
NimbusJwtDecoderbuilt from a client registration (it needs a live JWK set endpoint, so the tests inject a decoder factory), the warning log text when no login page is configured, and an end-to-end login against a real Keycloak server.