2

According to spring doc authentication for websocket falls back to authentication of HTTP request ( handshake ) and when spring-security is set up, no more configuration is needed.

I have set up the spring security:

@EnableWebSecurity
@EnableMethodSecurity
@EnableRedisIndexedHttpSession(maxInactiveIntervalInSeconds = 60 * 30)
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final UserDetailsService detailsService;
    private final RedisIndexedSessionRepository redisIndexedSessionRepository;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider authenticationProvider(PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder);
        provider.setUserDetailsService(this.detailsService);
        return provider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationProvider authenticationProvider) {
        return new ProviderManager(authenticationProvider);
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:3000")); //allows React to access the API from origin on port 3000. Change accordingly
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
        configuration.setAllowCredentials(true);
        configuration.addAllowedHeader("*");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }


    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf().disable()
                .cors(Customizer.withDefaults())
                .authorizeHttpRequests(auth -> {
                    auth.requestMatchers(
                            "/api/v1/auth/register/**",
                            "/api/v1/auth/login"
                    ).permitAll();
                    auth.anyRequest().authenticated();
                })
                .sessionManagement(sessionManagement -> sessionManagement
                        .sessionCreationPolicy(IF_REQUIRED) //
                        .sessionFixation(SessionManagementConfigurer.SessionFixationConfigurer::newSession) //
                        .maximumSessions(1) //
                        .sessionRegistry(sessionRegistry())
                )
                .logout(out -> out
                        .logoutUrl("/api/v1/auth/logout")
                        .invalidateHttpSession(true) // Invalidate all sessions after logout
                        .deleteCookies("JSESSIONID")
                        .addLogoutHandler(new CustomLogoutHandler(this.redisIndexedSessionRepository))
                        .logoutSuccessHandler((request, response, authentication) ->
                                SecurityContextHolder.clearContext()
                        )
                )
                .build();
    }

    @Bean
    public SpringSessionBackedSessionRegistry<? extends Session> sessionRegistry() {
        return new SpringSessionBackedSessionRegistry<>(this.redisIndexedSessionRepository);
    }

    /**
     * A SecurityContextRepository implementation which stores the security context in the HttpSession between requests.
     */
    @Bean
    public SecurityContextRepository securityContextRepository() {
        return new HttpSessionSecurityContextRepository();
    }
}

Basicly spring-security that uses REDIS to store sessions. This works for typical CRUD operations.

For websockets i have this config:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/user");
        registry.setApplicationDestinationPrefixes("/app");
        registry.setUserDestinationPrefix("/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*")
                .setAllowedOrigins("http://localhost:3000")
                .addInterceptors(new CustomHttpSessionHandshakeInterceptor())
                .withSockJS();
    }

    @Override
    public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
        DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
        resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();

        ObjectMapper objectMapper = new ObjectMapper();
        // method needed to add support for date types ( LocalDateTime ) - jackson-datatype-jsr310 dependency
        objectMapper.findAndRegisterModules();

        converter.setObjectMapper(objectMapper);
        converter.setContentTypeResolver(resolver);
        messageConverters.add(converter);
        return false;
    }
}

However the web-socket connection fails:

const connectWebSocket = () => {
    if (!user?.id) {
        console.log("No user logged")
        return;
    }

    client.current = new Client({
        brokerURL: 'ws://127.0.0.1:8080/ws/websocket',
        debug: function (str) {
            console.log(str);
        },
        onConnect: () => {
            console.log('WebSocket connection established');
            client.current?.subscribe(`/user/${user.id}/queue/messages`, message => {
                const receivedMessage: Message = JSON.parse(message.body);
                console.log('Received message from WebSocket:', receivedMessage);
                updateChatWithNewMessage(receivedMessage);
            });
        },
        onStompError: frame => {
            console.error('Broker reported error: ' + frame.headers['message']);
            console.error('Additional details: ' + frame.body);
        },
        onWebSocketClose: () => {
            console.log('WebSocket connection closed');
        },
    });
    client.current.activate();
};

BUT if i configure "/ws/**" endpoint to not require auhtentication :

       .authorizeHttpRequests(auth -> {
            auth.requestMatchers(
                    "/api/v1/auth/register/**",
                    "/api/v1/auth/login" ,
                    "/ws/**").permitAll();
            auth.anyRequest().authenticated();
        })

Connection is successful.

It seems like Stomp.js client is not sending credentails, and spring-security doesnt let the request pass through filters. Is there any way how to add the credentials? In my fetch request i am using

            {  
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                },
                credentials: 'include',
            }

to include credentials ( back end returns JSESSIONID cookie )

But i have not found any way how to include it in websocket connection.

Thanks for help!

//EDIT

According to browser's console, the JSESSIONID is indeed in Request headers for websocket, which confuses me even more why it doesnt work

0