) map
+ : Collections.emptyMap();
+ }
+
+ /**
+ * Creates a decoder for the access tokens of the given client registration,
+ * or {@code null} when the registration has no JWK set URI to verify them
+ * against.
+ */
+ private static JwtDecoder createDecoder(ClientRegistration registration) {
+ var providerDetails = registration.getProviderDetails();
+ var jwkSetUri = providerDetails.getJwkSetUri();
+ if (!StringUtils.hasText(jwkSetUri)) {
+ return null;
+ }
+ var issuerUri = providerDetails.getIssuerUri();
+ var decoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
+ decoder.setJwtValidator(StringUtils.hasText(issuerUri)
+ ? JwtValidators.createDefaultWithIssuer(issuerUri)
+ : JwtValidators.createDefault());
+ return decoder;
+ }
+}
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/KeycloakRoleMapping.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/KeycloakRoleMapping.java
new file mode 100644
index 00000000000..6291568de2f
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/KeycloakRoleMapping.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.spring.security;
+
+import java.util.function.Supplier;
+
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.oauth2.client.OAuth2LoginConfigurer;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+
+/**
+ * Makes an OAuth2 login use an {@link OidcUserService} that maps Keycloak roles
+ * with {@link KeycloakOidcUserMapper}.
+ *
+ * This lives apart from {@link VaadinSecurityConfigurer} on purpose. The
+ * {@code spring-security-oauth2-client} dependency that {@link OidcUserService}
+ * comes from is optional, and passing an {@code OidcUserService} to a parameter
+ * of type {@code OAuth2UserService} makes the verifier load the latter to check
+ * assignability. Doing that from {@code VaadinSecurityConfigurer} would break
+ * every application that configures Vaadin security without the dependency,
+ * because a class is verified as a whole when it is loaded. Keeping it here
+ * means the class is only loaded by an application that asks for Keycloak role
+ * mapping, which has the dependency anyway.
+ *
+ * @see VaadinSecurityConfigurer#keycloakRoleMapping()
+ */
+final class KeycloakRoleMapping {
+
+ private KeycloakRoleMapping() {
+ // Only static utility methods
+ }
+
+ /**
+ * Sets an {@link OidcUserService} that maps Keycloak roles on the given
+ * OAuth2 login, and shares it so that it can be inspected or replaced.
+ *
+ * @param loginConfigurer
+ * the OAuth2 login configurer to customize
+ * @param http
+ * the security builder to share the service with
+ * @param rolePrefix
+ * supplies the role prefix to use, resolved when a user is
+ * mapped
+ */
+ static void apply(OAuth2LoginConfigurer loginConfigurer,
+ HttpSecurity http, Supplier rolePrefix) {
+ var oidcUserService = new OidcUserService();
+ oidcUserService
+ .setOidcUserConverter(new KeycloakOidcUserMapper(rolePrefix));
+ http.setSharedObject(OidcUserService.class, oidcUserService);
+ loginConfigurer.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
+ .oidcUserService(oidcUserService));
+ }
+}
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurer.java
index 00c0a6a6d71..d652a09b09f 100644
--- a/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurer.java
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurer.java
@@ -133,6 +133,8 @@
* {@link VaadinDefaultRequestCache}
* {@link VaadinSavedRequestAwareAuthenticationSuccessHandler}
* {@link ClientRegistrationRepository}
+ * {@code OidcUserService}, when Keycloak role mapping is enabled with
+ * {@link #keycloakRoleMapping()}
*
*
* @since 24.8
@@ -159,6 +161,8 @@ public final class VaadinSecurityConfigurer
private String postLogoutRedirectUri;
+ private boolean keycloakRoleMapping = false;
+
private boolean enableCsrfConfiguration = true;
private boolean enableLogoutConfiguration = true;
@@ -316,6 +320,32 @@ public VaadinSecurityConfigurer oauth2LoginPage(String oauth2LoginPage,
return this;
}
+ /**
+ * 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 are not
+ * part of the authenticated user by default. This opts in to decoding the
+ * access token and mapping its roles, which makes
+ * {@code @RolesAllowed("admin")} and {@code hasRole("admin")} match a
+ * Keycloak role named {@code admin}. See {@link KeycloakOidcUserMapper} for
+ * what exactly is mapped.
+ *
+ * Works only together with {@link #oauth2LoginPage(String)} and its
+ * overloads, and sets the {@code OidcUserService} that this security filter
+ * chain uses to load the authenticated user. An application that has its
+ * own {@code OidcUserService} should leave this off and install the mapper
+ * on that service instead, as {@link KeycloakOidcUserMapper} shows.
+ *
+ * @return the current configurer instance for method chaining
+ * @see KeycloakOidcUserMapper
+ * @since 25.4
+ */
+ public VaadinSecurityConfigurer keycloakRoleMapping() {
+ this.keycloakRoleMapping = true;
+ return this;
+ }
+
/**
* Sets the default success URL after authentication. Redirected only when
* no protected page was previously accessed. Works only together with
@@ -553,7 +583,22 @@ public void init(HttpSecurity http) {
http.oauth2Login(configurer -> {
configurer.loginPage(oauth2LoginPage).permitAll();
configurer.successHandler(getAuthenticationSuccessHandler());
+ if (keycloakRoleMapping) {
+ // The role prefix holder is only populated with the prefix
+ // of the filter chain in configure(), which runs after
+ // this, so the prefix is resolved when a user is mapped
+ var rolePrefixHolder = getVaadinRolePrefixHolder();
+ KeycloakRoleMapping.apply(configurer, getBuilder(),
+ rolePrefixHolder != null
+ ? rolePrefixHolder::getRolePrefix
+ : () -> null);
+ }
});
+ } else if (keycloakRoleMapping) {
+ LOGGER.warn(
+ "Keycloak role mapping is enabled but no OAuth2 login page "
+ + "is configured, so it has no effect. Configure "
+ + "one with VaadinSecurityConfigurer.oauth2LoginPage().");
}
if (enableCsrfConfiguration) {
http.csrf(this::customizeCsrf);
diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java
index d3681b4ba31..9c890493e89 100644
--- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java
+++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/SpringClassesSerializableTest.java
@@ -105,6 +105,10 @@ protected Stream getExcludedPatterns() {
"com\\.vaadin\\.flow\\.spring\\.scopes\\.AbstractScope",
"com\\.vaadin\\.flow\\.spring\\.scopes\\.VaadinUIScope",
"com\\.vaadin\\.flow\\.spring\\.security\\.AuthenticationContext",
+ // Part of the security filter chain, not of session state,
+ // like the OidcUserService that holds it
+ "com\\.vaadin\\.flow\\.spring\\.security\\.KeycloakOidcUserMapper",
+ "com\\.vaadin\\.flow\\.spring\\.security\\.KeycloakRoleMapping",
"com\\.vaadin\\.flow\\.spring\\.security\\.NavigationAccessControlConfigurer",
"com\\.vaadin\\.flow\\.spring\\.security\\.VaadinAwareSecurityContextHolderStrategy",
"com\\.vaadin\\.flow\\.spring\\.security\\.VaadinAwareSecurityContextHolderStrategyConfiguration",
diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/KeycloakOidcUserMapperTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/KeycloakOidcUserMapperTest.java
new file mode 100644
index 00000000000..ced99d05c59
--- /dev/null
+++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/KeycloakOidcUserMapperTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2000-2026 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.flow.spring.security;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserSource;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails;
+import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails.UserInfoEndpoint;
+import org.springframework.security.oauth2.core.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
+import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
+import org.springframework.security.oauth2.jwt.BadJwtException;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+/**
+ * Specification of the opt-in mapping of Keycloak realm and client roles to
+ * Spring Security granted authorities.
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class KeycloakOidcUserMapperTest {
+
+ private static final String CLIENT_ID = "test-client";
+
+ private static final String ISSUER_URI = "http://localhost:8080/realms/test";
+
+ private static final String JWK_SET_URI = ISSUER_URI
+ + "/protocol/openid-connect/certs";
+
+ @Mock
+ private OidcUserSource userSource;
+
+ @Mock
+ private OidcUserRequest userRequest;
+
+ @Mock
+ private OidcUserInfo userInfo;
+
+ @Mock
+ private OidcIdToken idToken;
+
+ @Mock
+ private ClientRegistration clientRegistration;
+
+ @Mock
+ private ProviderDetails providerDetails;
+
+ @Mock
+ private UserInfoEndpoint userInfoEndpoint;
+
+ @Mock
+ private OAuth2AccessToken accessToken;
+
+ private final Map accessTokenClaims = new HashMap<>();
+
+ private final AtomicInteger decoderFactoryCalls = new AtomicInteger();
+
+ private KeycloakOidcUserMapper mapper;
+
+ @BeforeEach
+ void setup() {
+ mapper = new KeycloakOidcUserMapper(null, registration -> {
+ decoderFactoryCalls.incrementAndGet();
+ return this::decode;
+ });
+
+ when(userSource.getUserRequest()).thenReturn(userRequest);
+ when(userSource.getUserInfo()).thenReturn(userInfo);
+ when(userRequest.getClientRegistration())
+ .thenReturn(clientRegistration);
+ when(userRequest.getAccessToken()).thenReturn(accessToken);
+ when(userRequest.getIdToken()).thenReturn(idToken);
+ when(clientRegistration.getRegistrationId()).thenReturn("keycloak");
+ when(clientRegistration.getClientId()).thenReturn(CLIENT_ID);
+ when(clientRegistration.getProviderDetails())
+ .thenReturn(providerDetails);
+ when(providerDetails.getUserInfoEndpoint())
+ .thenReturn(userInfoEndpoint);
+ when(accessToken.getTokenValue()).thenReturn("token");
+ when(accessToken.getScopes()).thenReturn(Set.of("openid", "profile"));
+ when(idToken.getClaims())
+ .thenReturn(Map.of("sub", "user-123", "iss", ISSUER_URI));
+ }
+
+ @Test
+ void convert_realmAndClientRolesAndScopesMapped() {
+ accessTokenClaims.put("realm_access",
+ Map.of("roles", List.of("admin", "user")));
+ accessTokenClaims.put("resource_access",
+ Map.of(CLIENT_ID, Map.of("roles", List.of("manage-account"))));
+
+ var authorities = authorities();
+
+ assertThat(authorities).contains("ROLE_admin", "ROLE_user",
+ "ROLE_manage-account", "SCOPE_openid", "SCOPE_profile");
+ assertThat(mapper.convert(userSource).getAuthorities())
+ .hasAtLeastOneElementOfType(OidcUserAuthority.class);
+ }
+
+ @Test
+ void convert_otherClientRolesAndMissingClaimsIgnored() {
+ accessTokenClaims.put("resource_access",
+ Map.of("other-client", Map.of("roles", List.of("other-role"))));
+
+ assertThat(authorities()).noneMatch(a -> a.startsWith("ROLE_"));
+ }
+
+ @Test
+ void convert_customRolePrefixAppliedToRolesOnly() {
+ mapper = new KeycloakOidcUserMapper(() -> "AUTHORITY_",
+ registration -> this::decode);
+ accessTokenClaims.put("realm_access",
+ Map.of("roles", List.of("admin")));
+
+ assertThat(authorities()).contains("AUTHORITY_admin", "SCOPE_openid")
+ .doesNotContain("ROLE_admin");
+ }
+
+ @Test
+ void convert_accessTokenNotAJwt_mappedWithoutRoles() {
+ mapper = new KeycloakOidcUserMapper(null, registration -> token -> {
+ throw new BadJwtException("not a JWT");
+ });
+
+ var authorities = authorities();
+
+ assertThat(authorities).noneMatch(a -> a.startsWith("ROLE_"));
+ assertThat(authorities).contains("SCOPE_openid");
+ }
+
+ @Test
+ void convert_userNameAttributeUsedAsName_userInfoRetained() {
+ when(userInfoEndpoint.getUserNameAttributeName())
+ .thenReturn("preferred_username");
+ when(userInfo.getClaims()).thenReturn(Map.of("preferred_username",
+ "john", "email", "john@example.com"));
+
+ var user = mapper.convert(userSource);
+
+ // The name attribute is only in the userinfo response, so dropping it
+ // would both lose claims and fail the login
+ assertThat(user.getName()).isEqualTo("john");
+ assertThat(user.getUserInfo()).isSameAs(userInfo);
+ assertThat(user.getClaims()).containsEntry("email", "john@example.com");
+ }
+
+ @Test
+ void convert_registrationWithoutJwkSetUri_mappedWithoutRoles() {
+ // There is nothing to verify the access token against, so it is left
+ // alone rather than failing the login
+ mapper = new KeycloakOidcUserMapper();
+
+ var authorities = authorities();
+
+ assertThat(authorities).noneMatch(a -> a.startsWith("ROLE_"));
+ assertThat(authorities).contains("SCOPE_openid");
+ }
+
+ @Test
+ void convert_registrationWithoutIssuerUri_mappedWithoutRoles() {
+ // A registration set up with explicit endpoints has no issuer URI, and
+ // building the default validator with a null issuer must not fail the
+ // login
+ when(providerDetails.getJwkSetUri()).thenReturn(JWK_SET_URI);
+ mapper = new KeycloakOidcUserMapper();
+
+ var authorities = authorities();
+
+ assertThat(authorities).noneMatch(a -> a.startsWith("ROLE_"));
+ assertThat(authorities).contains("SCOPE_openid");
+ }
+
+ @Test
+ void convert_decoderCreatedOncePerClientRegistration() {
+ mapper.convert(userSource);
+ mapper.convert(userSource);
+
+ assertThat(decoderFactoryCalls).hasValue(1);
+ }
+
+ private List authorities() {
+ return mapper.convert(userSource).getAuthorities().stream()
+ .map(GrantedAuthority::getAuthority).toList();
+ }
+
+ private Jwt decode(String tokenValue) {
+ var issuedAt = Instant.parse("2026-01-01T00:00:00Z");
+ // @formatter:off
+ return Jwt.withTokenValue(tokenValue)
+ .header("alg", "RS256")
+ .issuer(ISSUER_URI)
+ .issuedAt(issuedAt)
+ .expiresAt(issuedAt.plusSeconds(60))
+ .subject("user-123")
+ .claims(claims -> claims.putAll(accessTokenClaims))
+ .build();
+ // @formatter:on
+ }
+}
diff --git a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java
index acea2e50ead..c8058950ccc 100644
--- a/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java
+++ b/vaadin-spring/src/test/java/com/vaadin/flow/spring/security/VaadinSecurityConfigurerTest.java
@@ -20,6 +20,7 @@
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
@@ -59,6 +60,7 @@
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.web.access.ExceptionTranslationFilter;
@@ -71,6 +73,7 @@
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
+import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
@@ -186,6 +189,51 @@ void oauth2LoginPage_chainHasAuthenticationFilter() {
OAuth2LoginAuthenticationFilter.class);
}
+ @Test
+ void keycloakRoleMapping_withOAuth2LoginPage_oidcUserServiceMapsRoles()
+ throws Exception {
+ http.with(configurer,
+ c -> c.oauth2LoginPage("/oauth2/authorization/keycloak")
+ .keycloakRoleMapping())
+ .build();
+
+ var oidcUserService = http.getSharedObject(OidcUserService.class);
+
+ assertThat(oidcUserService).isNotNull();
+ assertThat(getOidcUserConverter(oidcUserService))
+ .isInstanceOf(KeycloakOidcUserMapper.class);
+ }
+
+ @Test
+ void keycloakRoleMapping_withoutOAuth2LoginPage_notConfigured() {
+ http.with(configurer, VaadinSecurityConfigurer::keycloakRoleMapping)
+ .build();
+
+ assertNull(http.getSharedObject(OidcUserService.class));
+ }
+
+ @Test
+ void keycloakRoleMapping_rolePrefixOfChain_isUsedForRoles()
+ throws Exception {
+ var rolePrefixHolder = new VaadinRolePrefixHolder(null);
+ http.setSharedObject(VaadinRolePrefixHolder.class, rolePrefixHolder);
+
+ http.with(configurer,
+ c -> c.oauth2LoginPage("/oauth2/authorization/keycloak")
+ .keycloakRoleMapping())
+ .build();
+ // The prefix of the filter chain is only known to the holder after the
+ // chain has been configured, so the mapper must pick it up afterwards
+ var securityContextFilter = new SecurityContextHolderAwareRequestFilter();
+ securityContextFilter.setRolePrefix("AUTHORITY_");
+ rolePrefixHolder.resetRolePrefix(securityContextFilter);
+
+ var mapper = (KeycloakOidcUserMapper) getOidcUserConverter(
+ http.getSharedObject(OidcUserService.class));
+
+ assertThat(mapper.rolePrefix()).isEqualTo("AUTHORITY_");
+ }
+
@Test
void logoutSuccessHandler_handlerIsConfigured(
@Mock LogoutSuccessHandler handler) {
@@ -486,6 +534,17 @@ void defaultSuccessUrl_notSet_usesRootPath() throws Exception {
assertThat(isAlwaysUseDefaultTargetUrl(handler)).isFalse();
}
+ @Test
+ void withoutOAuth2ClientOnClasspath_configurerStillLinks() {
+ // spring-security-oauth2-client is an optional dependency, and a class
+ // is verified as a whole when it is loaded, so a reference to one of
+ // its types here would break every application that does not have it
+ assertThatCode(
+ () -> Class.forName(VaadinSecurityConfigurer.class.getName(),
+ true, new OAuth2ClientHidingClassLoader()))
+ .doesNotThrowAnyException();
+ }
+
@ParameterizedTest
@ValueSource(strings = { "style", "script", "image", "font" })
void anonymousSubResourceRequest_respondsWithUnauthorized(String fetchDest)
@@ -528,7 +587,66 @@ private MockHttpServletResponse sendAnonymousGetRequest(String path,
return mockResponse;
}
+ /**
+ * Loads the Vaadin security classes itself, so that they are verified
+ * against a classpath without {@code spring-security-oauth2-client}.
+ */
+ private static class OAuth2ClientHidingClassLoader extends ClassLoader {
+
+ private static final String HIDDEN_PACKAGE = "org.springframework.security.oauth2.client.";
+
+ private static final String RELOADED_PACKAGE = "com.vaadin.flow.spring.security.";
+
+ OAuth2ClientHidingClassLoader() {
+ super(VaadinSecurityConfigurer.class.getClassLoader());
+ }
+
+ @Override
+ protected Class> loadClass(String name, boolean resolve)
+ throws ClassNotFoundException {
+ if (name.startsWith(HIDDEN_PACKAGE)) {
+ throw new ClassNotFoundException(name);
+ }
+ if (name.startsWith(RELOADED_PACKAGE)) {
+ synchronized (getClassLoadingLock(name)) {
+ var loaded = findLoadedClass(name);
+ if (loaded == null) {
+ loaded = defineClass(name, readBytes(name));
+ }
+ if (resolve) {
+ resolveClass(loaded);
+ }
+ return loaded;
+ }
+ }
+ return super.loadClass(name, resolve);
+ }
+
+ private Class> defineClass(String name, byte[] bytes) {
+ return defineClass(name, bytes, 0, bytes.length);
+ }
+
+ private byte[] readBytes(String name) throws ClassNotFoundException {
+ var resource = name.replace('.', '/') + ".class";
+ try (var stream = getParent().getResourceAsStream(resource)) {
+ if (stream == null) {
+ throw new ClassNotFoundException(name);
+ }
+ return stream.readAllBytes();
+ } catch (IOException e) {
+ throw new ClassNotFoundException(name, e);
+ }
+ }
+ }
+
// Helper methods to access protected fields using reflection
+ private Object getOidcUserConverter(OidcUserService oidcUserService)
+ throws Exception {
+ var field = OidcUserService.class.getDeclaredField("oidcUserConverter");
+ field.setAccessible(true);
+ return field.get(oidcUserService);
+ }
+
private String getDefaultTargetUrl(
VaadinSavedRequestAwareAuthenticationSuccessHandler handler)
throws Exception {