According to spring doc ( https://docs.spring.io/spring-framework/reference/web/websocket/stomp/authentication.html ) 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.
When i inspect the browser's network console the SESSIONID is sent as request header, meaning spring-security should have all required data to authorize the request same as for typical CRUD operation, yet it does not. .
Comment From: bclozel
Thanks for getting in touch, but it feels like this is a question that would be better suited to Stack Overflow. As mentioned in the guidelines for contributing, we prefer to use the issue tracker only for bugs and enhancements. Feel free to update this issue with a link to the re-posted question (so that other people can find it) or add some more details if you feel this is a genuine bug.
In this case, there is no way for us to easily reproduce the problem you are seeing. Given the number of dependencies and complexity of your case it is more likely to be a configuration problem. Maybe you can reduce this a bit and submit it as a StackOverflow question?
We can reopen this issue if you can provide a minimal sample that reproduces the problem. Typically, the sample would need to use explain HTTP session (no redis involved) and reduce the codebase to a minimum.
Comment From: Darlynnnn
If documentation specify that web security works for websockets(handshake) too, and given configuration works for REST operations but does not for websocket connection IT means either
1) documentation IS wrong - and lacks proper explanation/examples 2) there IS a Bug in framework itself
Both of the cases belongs here imho.
Comment From: bclozel
Indeed. We'll wait for your minimal sample that demonstrates the issue and we will reopen it as a bug.