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