From 36d87089491d62b42cbf1f4a26f7eee6daed6a35 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:06:26 +0000 Subject: [PATCH 1/3] docs: document opt-in Keycloak role mapping for OAuth2 login Keycloak puts realm and client roles into the access token, so Spring Security maps neither and role checks silently fail. The new VaadinSecurityConfigurer.keycloakRoleMapping() method opts a filter chain in to mapping them, and KeycloakOidcUserMapper does the same for an application that builds its own OidcUserService. Documents https://github.com/vaadin/flow/pull/25627 Co-Authored-By: Claude Opus 5 (1M context) --- articles/flow/integrations/spring/oauth2.adoc | 75 +++++++++++++++++++ .../security/vaadin-security-configurer.adoc | 9 +++ 2 files changed, 84 insertions(+) diff --git a/articles/flow/integrations/spring/oauth2.adoc b/articles/flow/integrations/spring/oauth2.adoc index f6c32449a4..908050863f 100644 --- a/articles/flow/integrations/spring/oauth2.adoc +++ b/articles/flow/integrations/spring/oauth2.adoc @@ -114,4 +114,79 @@ class SecurityConfiguration { The [methodname]`oauth2LoginPage(String)` method is a shortcut that defaults the post-logout redirect URL to `{baseUrl}`. +[[keycloak-role-mapping]] +[role="since:com.vaadin:vaadin@V25.4"] +== Keycloak Role Mapping + +Keycloak puts the roles of a user into the access token rather than into the ID token, and it does so in the `realm_access` and `resource_access` claims, which aren't part of the OpenID Connect specification. Spring Security maps neither of them, so `@RolesAllowed("admin")` and `hasRole("admin")` don't match a Keycloak role named `admin`, and every user is rejected. + +Mapping those roles is opt-in per security filter chain. Enable it by calling the [methodname]`keycloakRoleMapping()` method of [classname]`VaadinSecurityConfigurer`: + +.Enable Keycloak Role Mapping +[source,java] +---- + +@Configuration +class SecurityConfiguration { + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.with(VaadinSecurityConfigurer.vaadin(), configurer -> { + configurer.oauth2LoginPage("/oauth2/authorization/keycloak") + .keycloakRoleMapping(); // (1) + }); + return http.build(); + } +} +---- + +<1> Decodes the access token of the authenticated user and maps its roles to granted authorities. + +Views and services can then be guarded with the plain Keycloak role name: + +[source,java] +---- +@Route("admin") +@RolesAllowed("admin") // Matches the Keycloak realm role "admin" +public class AdminView extends VerticalLayout { +} +---- + +The mapping grants the following authorities: + +- The realm roles from the `realm_access` claim of the access token. +- The roles that the `resource_access` claim grants for the client ID of the current client registration. Roles that it grants for other clients are ignored. +- The scopes of the access token, prefixed with `SCOPE_`, as the default user service does. + +Realm roles and client roles both become role authorities that use the role prefix of the application, which is `ROLE_` unless a [classname]`GrantedAuthorityDefaults` bean defines another prefix. + +[NOTE] +==== +The [methodname]`keycloakRoleMapping()` method works only together with [methodname]`oauth2LoginPage()` and its overloads. Without a login page for OAuth2 authentication, it logs a warning and has no effect. +==== + +Verifying the access token requires the JSON Web Key Set (JWK) of the provider, which a client registration resolves from its `issuer-uri`. If the registration has no JWK set URI, or if the access token isn't a JWT that the application can decode, the login still succeeds, but the user is mapped without any roles. Both cases are logged at debug level. + +When the mapping is enabled, the configurer builds an [classname]`OidcUserService` for the OAuth2 login and shares it with the [classname]`HttpSecurity` instance, so it can be retrieved with `http.getSharedObject(OidcUserService.class)`. + + +=== Mapping Roles on a Custom User Service + +Enabling the mapping replaces the [classname]`OidcUserService` of the filter chain. An application that builds its own user service should therefore leave [methodname]`keycloakRoleMapping()` off and install the [classname]`KeycloakOidcUserMapper` converter on that service instead: + +[source,java] +---- +var oidcUserService = new OidcUserService(); +oidcUserService.setOidcUserConverter(new KeycloakOidcUserMapper()); +---- + +The mapper prefixes roles with `ROLE_`. To apply another prefix -- for example, to match a [classname]`GrantedAuthorityDefaults` bean -- pass it to the constructor: + +[source,java] +---- +new KeycloakOidcUserMapper("AUTHORITY_"); +---- + +Configure the resulting user service on the OAuth2 login as described in the https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html[Spring Security documentation]. + + [discussion-id]`EF8F6AC3-BE67-4BE2-9A78-C371C1D4B9FD` diff --git a/articles/flow/security/vaadin-security-configurer.adoc b/articles/flow/security/vaadin-security-configurer.adoc index e4403af724..499913e34c 100644 --- a/articles/flow/security/vaadin-security-configurer.adoc +++ b/articles/flow/security/vaadin-security-configurer.adoc @@ -56,6 +56,8 @@ The following beans are shared by this configurer (if not already shared): * `VaadinRolePrefixHolder` — Holds role prefix accessible outside an active request * `VaadinDefaultRequestCache` — A request cache implementation which ignores requests that are not for routes * `VaadinSavedRequestAwareAuthenticationSuccessHandler` — A strategy that uses an available VaadinSession for retrieving the security context +* `ClientRegistrationRepository` — The repository of the OAuth2 client registrations of the application +* `OidcUserService` — The service that loads the authenticated user, shared only when Keycloak role mapping is enabled with `keycloakRoleMapping()` ==== Configuration Methods @@ -105,6 +107,13 @@ public VaadinSecurityConfigurer oauth2LoginPage(String oauth2LoginPage, String p Configures the login page for OAuth2 authentication and the post-logout redirect URI. +[source,java] +---- +public VaadinSecurityConfigurer keycloakRoleMapping() +---- + +[since:com.vaadin:vaadin@V25.4]#Enables mapping of Keycloak realm and client roles to Spring Security granted authorities (disabled by default).# Keycloak puts the roles of a user into the access token, so they aren't part of the authenticated user by default. Enabling the mapping decodes the access token and maps its roles, which makes `@RolesAllowed("admin")` and `hasRole("admin")` match a Keycloak role named `admin`. Works only together with `oauth2LoginPage(String)` and its overloads. See <<{articles}/flow/integrations/spring/oauth2#keycloak-role-mapping,Keycloak Role Mapping>> for details. + ===== Logout Configuration [source,java] From 71e1d719f9f32d6da18d7a3f0061e4dce5453b55 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 13:10:10 +0000 Subject: [PATCH 2/3] docs: clarify the authorities and the JWKS source of Keycloak role mapping The list of granted authorities read as exhaustive but left out the OidcUserAuthority, which the mapper keeps like the default user service, so enabling the mapping looked like it drops OIDC_USER. The JWK set URI can also be configured directly instead of being resolved from the issuer URI, and the abbreviation for a JSON Web Key Set is JWKS. Co-Authored-By: Claude Opus 5 (1M context) --- articles/flow/integrations/spring/oauth2.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/articles/flow/integrations/spring/oauth2.adoc b/articles/flow/integrations/spring/oauth2.adoc index 908050863f..6f1bef92e8 100644 --- a/articles/flow/integrations/spring/oauth2.adoc +++ b/articles/flow/integrations/spring/oauth2.adoc @@ -155,7 +155,8 @@ The mapping grants the following authorities: - The realm roles from the `realm_access` claim of the access token. - The roles that the `resource_access` claim grants for the client ID of the current client registration. Roles that it grants for other clients are ignored. -- The scopes of the access token, prefixed with `SCOPE_`, as the default user service does. +- The scopes of the access token, prefixed with `SCOPE_`, as the default user service grants them. +- The [classname]`OidcUserAuthority` of the authenticated user, built from the ID token and the userinfo claims, as the default user service grants it. Realm roles and client roles both become role authorities that use the role prefix of the application, which is `ROLE_` unless a [classname]`GrantedAuthorityDefaults` bean defines another prefix. @@ -164,7 +165,7 @@ Realm roles and client roles both become role authorities that use the role pref The [methodname]`keycloakRoleMapping()` method works only together with [methodname]`oauth2LoginPage()` and its overloads. Without a login page for OAuth2 authentication, it logs a warning and has no effect. ==== -Verifying the access token requires the JSON Web Key Set (JWK) of the provider, which a client registration resolves from its `issuer-uri`. If the registration has no JWK set URI, or if the access token isn't a JWT that the application can decode, the login still succeeds, but the user is mapped without any roles. Both cases are logged at debug level. +Verifying the access token requires the JSON Web Key Set (JWKS) of the provider, which a client registration either resolves from its `issuer-uri` or takes from an explicitly configured `jwk-set-uri`. If the registration has no JWKS URI, or if the access token isn't a JWT that the application can decode, the login still succeeds, but the user is mapped without any roles. Both cases are logged at debug level. When the mapping is enabled, the configurer builds an [classname]`OidcUserService` for the OAuth2 login and shares it with the [classname]`HttpSecurity` instance, so it can be retrieved with `http.getSharedObject(OidcUserService.class)`. From 60e906de135d07163a6c2dba4a7ca78619984ea6 Mon Sep 17 00:00:00 2001 From: "totally-not-ai[bot]" <290682512+totally-not-ai[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 08:06:05 +0000 Subject: [PATCH 3/3] docs: soften the Keycloak claim wording in the role mapping section Keycloak puts the roles into the access token by default, not always: a client scope mapper can add them to the ID token too. Without the mapping, users are not rejected altogether, only the views and methods that check a role deny access. The role prefix can also come from the servletApi() configuration of the filter chain. Co-Authored-By: Claude Opus 5 (1M context) --- articles/flow/integrations/spring/oauth2.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/articles/flow/integrations/spring/oauth2.adoc b/articles/flow/integrations/spring/oauth2.adoc index 6f1bef92e8..c380ad8cb7 100644 --- a/articles/flow/integrations/spring/oauth2.adoc +++ b/articles/flow/integrations/spring/oauth2.adoc @@ -118,7 +118,7 @@ The [methodname]`oauth2LoginPage(String)` method is a shortcut that defaults the [role="since:com.vaadin:vaadin@V25.4"] == Keycloak Role Mapping -Keycloak puts the roles of a user into the access token rather than into the ID token, and it does so in the `realm_access` and `resource_access` claims, which aren't part of the OpenID Connect specification. Spring Security maps neither of them, so `@RolesAllowed("admin")` and `hasRole("admin")` don't match a Keycloak role named `admin`, and every user is rejected. +By default, Keycloak puts the roles of a user into the access token rather than into the ID token, and it does so in the `realm_access` and `resource_access` claims, which aren't part of the OpenID Connect specification. Spring Security maps neither of them, so `@RolesAllowed("admin")` and `hasRole("admin")` never match a Keycloak role named `admin`, and access to the views and methods that they protect is denied. Mapping those roles is opt-in per security filter chain. Enable it by calling the [methodname]`keycloakRoleMapping()` method of [classname]`VaadinSecurityConfigurer`: @@ -158,7 +158,7 @@ The mapping grants the following authorities: - The scopes of the access token, prefixed with `SCOPE_`, as the default user service grants them. - The [classname]`OidcUserAuthority` of the authenticated user, built from the ID token and the userinfo claims, as the default user service grants it. -Realm roles and client roles both become role authorities that use the role prefix of the application, which is `ROLE_` unless a [classname]`GrantedAuthorityDefaults` bean defines another prefix. +Realm roles and client roles both become role authorities that use the role prefix of the application, which is `ROLE_` unless a [classname]`GrantedAuthorityDefaults` bean or the [methodname]`servletApi()` configuration of the filter chain sets another prefix. [NOTE] ====