feat(spring): add opt-in Keycloak role mapping to VaadinSecurityConfigurer - #25627
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.
| "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", | ||
| "cpu": [ | ||
| "arm" | ||
| ], |
There was a problem hiding this comment.
This file should not change in this PR
…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.
heruan
left a comment
There was a problem hiding this comment.
Suggestions to simplify the port, nothing blocking.
Stop reusing an OidcUserService shared with the security builder: Spring Security looks up a bean of that type rather than a shared object, so the contract was new here and nothing set it, and taking over a service the application owns is surprising. The mapping now always creates its own service and shares it as an output, and a custom service is pointed at setOidcUserConverter instead. Also stop failing the login for a client registration configured with explicit endpoints, where the JWK set URI and the issuer URI are null: building the decoder threw an IllegalArgumentException, which is not a JwtException and so escaped the catch that maps a user without roles.
A client registration without a JWK set URI has no decoder, and computeIfAbsent stores no mapping for a null result, so one was built again on every login. Store an Optional so the lookup happens once, as the javadoc says it does. Also cover the client registration that has a JWK set URI but no issuer URI: building the default validator for a null issuer throws an IllegalArgumentException, which no test reached.
|
@totally-not-ai resolve conflicts with main and revert changes in package-lock files that are unrelevant. |
…pping # Conflicts: # vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java
|
@mshabarov Both done. Merged On the lock files: there are none left in the diff. What the merge did drag in was a copyright-year rewrite across the 123 freshly imported |
|
merge main |
…pping # Conflicts: # vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java
|
@Artur- Merged. Three conflicts, all in
|
|
heruan
left a comment
There was a problem hiding this comment.
Re-approving after the merges from main. The six vaadin-spring files are unchanged since the last review, the conflict resolution in VaadinSecurityConfigurerTest kept both sides, and the diff no longer touches any lock file.
Add a "Mapping Keycloak Roles to Authorities" section to the OAuth2 integration page, and document the keycloakRoleMapping() method and OidcUserService shared object on the VaadinSecurityConfigurer reference page. Documents vaadin/flow#25627 (`375764465d2d84e51bfd9038528b964830be2d3c`).
|
Pull request created: #6110
|
|
Documentation Bot: Draft documentation pull request for this change: #aw_docspr Files updated:
It was written from the state of this pull request as you see it now. Please review it and mark it ready for review.
|
## Summary Documents the new opt-in Keycloak role mapping added in Vaadin 25.4. Keycloak puts user roles into the access token, so Spring Security ignores them and role checks fail silently; the docs now explain how to turn the mapping on. ## What changed Documentation only — no code, no behavior change. **OAuth2 guide** (`articles/flow/integrations/spring/oauth2.adoc`): new "Keycloak Role Mapping" section that explains why Keycloak roles don't match by default, shows how to enable `keycloakRoleMapping()` on `VaadinSecurityConfigurer`, and lists the authorities the mapping grants (realm roles, client roles for the current client ID, `SCOPE_` scopes, and the `OidcUserAuthority`). It also covers the role prefix, the JWKS requirement for decoding the access token, the fallback when no roles can be read, and the shared `OidcUserService`. A subsection shows how to use `KeycloakOidcUserMapper` directly when the application builds its own `OidcUserService`. **Configurer reference** (`articles/flow/security/vaadin-security-configurer.adoc`): adds `keycloakRoleMapping()` to the configuration methods, and lists `ClientRegistrationRepository` and `OidcUserService` among the shared beans. Documents vaadin/flow#25627. --------- Co-authored-by: totally-not-ai[bot] <290682512+totally-not-ai[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>



Summary
Keycloak sends a user's roles in the access token, so Vaadin applications never saw them and role-based access control did not work. This adds an opt-in switch,
VaadinSecurityConfigurer.keycloakRoleMapping(), that reads those roles and turns them into Spring Security authorities.What changed
Fully backward compatible. Nothing changes unless an application calls the new
keycloakRoleMapping()method. It is off by default.KeycloakOidcUserMapper, ported from SSO Kit. It decodes the access token and adds the realm roles (realm_access), the client roles granted to the current client id (resource_access), and the token scopes asSCOPE_authorities. Roles thatresource_accessgrants to other clients are ignored.VaadinSecurityConfigurer.keycloakRoleMapping()opts one security filter chain in. It only works together withoauth2LoginPage(...); if no OAuth2 login page is configured, the configurer logs a warning and does nothing.OidcUserServiceand shares it as a shared object, so it can be inspected or replaced. An application that owns its ownOidcUserServiceshould leave the switch off and set the mapper withsetOidcUserConverter(...)instead.VaadinRolePrefixHolder(resolved when a user is mapped, so a prefix defined by the filter chain itself is picked up) instead of a hardcodedROLE_, and a login is never failed when the access token cannot be decoded — the user is simply mapped without role authorities.VaadinSecurityConfigurerstill loads for applications without the optionalspring-security-oauth2-clientdependency.Use case
An application uses Keycloak for login and protects its admin views with
@RolesAllowed("admin"). Today those views stay blocked even for users who have theadminrealm role, because the role never reaches Spring Security. Adding one call fixes it.An application that builds its own
OidcUserServicecan use the mapper directly instead:API Changes
com.vaadin.flow.spring.security.KeycloakOidcUserMapper
com.vaadin.flow.spring.security.VaadinSecurityConfigurer
Test summary
VaadinSecurityConfigurerstill loads when the optionalspring-security-oauth2-clientdependency is absent.