Search code examples
spring-securityvaadinkeycloakvaadin-flowvaadin23

Vaadin 23 Spring Security with Keycloak - redirect user after login to the correct page


I configured Vaadin 23 application with Spring Security and Keyclock. Everything works fine except the users are not redirect to the page where they initiated the login process. The user is always redirected to the home page.

This is a SecurityConfiguration:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends VaadinWebSecurity {

    private final ClientRegistrationRepository clientRegistrationRepository;
    private final GrantedAuthoritiesMapper authoritiesMapper;
    private final ProfileService profileService;

    private String jwtAuthSecret;

    SecurityConfiguration(@Value("${spring.security.jwt.auth.secret}") String jwtAuthSecret, ClientRegistrationRepository clientRegistrationRepository,
                          GrantedAuthoritiesMapper authoritiesMapper, ProfileService profileService) {
        this.jwtAuthSecret = jwtAuthSecret;
        this.clientRegistrationRepository = clientRegistrationRepository;
        this.authoritiesMapper = authoritiesMapper;
        this.profileService = profileService;
        SecurityContextHolder.setStrategyName(VaadinAwareSecurityContextHolderStrategy.class.getName());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                // Enable OAuth2 login
                .oauth2Login(oauth2Login ->
                        oauth2Login
                                .clientRegistrationRepository(clientRegistrationRepository)
                                .userInfoEndpoint(userInfoEndpoint ->
                                        userInfoEndpoint
                                                .userAuthoritiesMapper(authoritiesMapper)
                                )
                                .loginPage("/login")
                                .successHandler(new KeycloakVaadinAuthenticationSuccessHandler(profileService))
                )
                // Configure logout
                .logout(logout ->
                        logout
                                .logoutSuccessHandler(logoutSuccessHandler())
                                .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
                ).sessionManagement(sessionManagement -> {
                    sessionManagement.sessionConcurrency(concurrency -> {

                        concurrency.maximumSessions(-1);

                        concurrency.sessionRegistry(sessionRegistry());

                        final var expiredStrategy = new UidlExpiredSessionStrategy();
                        concurrency.expiredSessionStrategy(expiredStrategy);
                    });
                });

        setStatelessAuthentication(http, new SecretKeySpec(Base64.getDecoder().decode(jwtAuthSecret), JwsAlgorithms.HS256), "com.example");
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    @Primary
    public SpringViewAccessChecker springViewAccessChecker(AccessAnnotationChecker accessAnnotationChecker) {
        return new KeycloakSpringViewAccessChecker(accessAnnotationChecker, "/oauth2/authorization/keycloak");
    }

    private OidcClientInitiatedLogoutSuccessHandler logoutSuccessHandler() {
        var logoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository);
        logoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
        return logoutSuccessHandler;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring().antMatchers("/session-expired", "/images/*", "/login", "/favicon.ico");
    }

    @Bean
    public PolicyFactory htmlSanitizer() {
        return Sanitizers.FORMATTING.and(Sanitizers.BLOCKS).and(Sanitizers.STYLES).and(Sanitizers.LINKS);
    }

}

How to properly redirect user to the original page?

UPDATED

public class KeycloakVaadinAuthenticationSuccessHandler extends VaadinSavedRequestAwareAuthenticationSuccessHandler {

    private static final Logger logger = LoggerFactory.getLogger(KeycloakVaadinAuthenticationSuccessHandler.class);

    private final ServiceFacade serviceFacade;

    public KeycloakVaadinAuthenticationSuccessHandler(ServiceFacade serviceFacade) {
        this.serviceFacade = serviceFacade;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        if (authentication == null || !(authentication instanceof OAuth2AuthenticationToken)) {
            String message = String.format("Authentication is null or not an instance of OAuth2AuthenticationToken: %s", authentication);
            logger.error(message);
            throw new IllegalStateException(message);
        }

        Collection<VaadinSession> vaadinSessions = VaadinSession.getAllSessions(request.getSession());

        OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
        String keycloakSessionId = (String) token.getPrincipal().getAttributes().get("sid");

        serviceFacade.getProfileService().createUserWithProfileIfNotExists((OAuth2AuthenticationToken) authentication, (user, profile, principalUserUuid) -> {
            try {

                if (CollectionUtils.isNotEmpty(vaadinSessions)) {
                    for (VaadinSession vaadinSession : vaadinSessions) {
                        if (vaadinSession.getService() != null) {
                            vaadinSession.access(() -> {
                                vaadinSession.setAttribute(UserInfo.SUB_PROPERTY, user.getUuid());
                                vaadinSession.setAttribute(UserInfo.KEYCLOAK_SESSION_ID, keycloakSessionId);
                            });
                        }
                    }
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                throw new IOException(e);
            }
        });

        super.onAuthenticationSuccess(request, response, authentication);
    }

Solution

  • The success handler is the one taking care of redirecting to the original view. You are overriding that with your own version so it will not work out of the box. There is nowadays a setOAuth2LoginPage helper in VaadinWebSecurity that will set up the correct success handler.