My goal is to secure a WebSocket endpoint e.g ws://localhost:8080/chat
.
What I did:
I tried to create WebSocket connection with STOMP
var socket = new SockJS("/chat"); stompClient = Stomp.over(socket); stompClient.connect({"X-XSRF-TOKEN": getCookie("XSRF-TOKEN")}, function (status) { // it should not execute. });
AbstractSecurityWebSocketMessageBrokerConfigurer
// see: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#websocket @Configuration public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { messages .nullDestMatcher().authenticated() .simpDestMatchers("/app/**").hasRole("USER") .simpDestMatchers("/user/**", "/topic/channel/*").hasRole("USER") .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() .anyMessage().denyAll(); } }
WebSocketMessageBrokerConfigurer
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat").withSockJS(); // user connect to } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); // user send message to registry.enableSimpleBroker("/topic", "/queue"); // user subscribe to .. for channel messages. registry.setUserDestinationPrefix("/user"); // user subscribe to .. for private messages. } }
WebSecurityConfigurerAdapter
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .formLogin() .and() .authorizeRequests().anyRequest().authenticated(); } }
My expected result is: The WebSocket connection should fail because I did not send the JSESSIONID.
My actual result is: The Spring Security recognize the user, and I can get the UserDetails
with (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()
.
My questions:
If I wish to use Database, which should I store, the JSESSIONID or the XSRF-TOKEN ?
Is using
X-XSRF-TOKEN
the right way to secure StompCommand.CONNECT endpoint?Why the Spring Security ask for
X-XSRF-TOKEN
instead ofJSESSIONID
. Isn’t CSRF token used to prevent Cross-Site Request Forgery?This is the error I get from the Browser’s console if I create a WebSocket connection without
X-XSRF-TOKEN
‘s key in the request header.<<< ERROR message:Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.web.csrf.InvalidCsrfTokenExceptionc Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'. content-length:0
Advertisement
Answer
First, I will try to help you with WS connection. I tried configuration similar to yours and I was unable to connect to WS unauthorized. I confirmed that in browser there was no JSESSIONID cookie or when there was one, it was connected to anonymous session. Try setting logging of org.springframework.security to TRACE and follow written log, it will help you determine which filter authorised access, if that is the case.
For questions that you specified:
JSESSIONID and XSRF-TOKEN are not something that are interchangeable. I believe that for same JSESSIONID you can receive different different XSRF-TOKEN cookies. So I think you should store session id. Spring has project for this: Spring Session JDBC
I think that it is. Even Spring Security expects it by default. Spring Security Websocket
Any inbound CONNECT message requires a valid CSRF token to enforce Same Origin Policy
- I am not sure how familiar are you with the Cross-Site Request Forgery. If you are not, check this Spring CSRF, but in few sentences:
JSESSIONID is cookie and it will be sent with every request to your site, even from other sites. Because that is how browser works. So to prevent misusage of session, CSRF token is required. Because you are not using HTML to execute action, such as form submit, you need to pass this token some other way. Spring requires you to send it using Header named X-XSRF-TOKEN, because browser will not send it with every request like it would a cookie. Security is in that other site, which tries to exploit vulnerability, can not read cookie from your site and it can not use it to add CSRF in header for exploit request.
So, I think your solution is ok. Keep in mind, that HTTP and WS are different protocols, even if WS uses HTTP for handshake, but it adds multiple headers. Maybe because of that Spring ignores authority checks, but I think that you probably missed something and that spring security trace log will direct you what to check.