6

I have a Spring Boot 2.2 MVC application with Spring-Security / Session and Spring-Websocket.
It's configured to only allow websocket connections when authenticated.

Following is the recommended way to create a Websocket Client in unit testing. (At least i have seen this in the spring docs)

private StompSession createWebsocket() throws InterruptedException, ExecutionException, TimeoutException {

    List<Transport> transports = new ArrayList<>(1);
    transports.add(new WebSocketTransport(new StandardWebSocketClient()));
    WebSocketStompClient stompClient = new WebSocketStompClient(new SockJsClient(transports));
    stompClient.setMessageConverter(new MappingJackson2MessageConverter());

    StompSession stompSession = stompClient.connect("ws://localhost:" + port + "/app",  new  StompSessionHandlerAdapter() {}).get(1, SECONDS);
    return stompSession;
}

The Problem

But, this way i get the exception in the logs, because Spring redirects to the Login page, which is in general ok and desired, because the Websocket Request was unauthenticated.
The logs confirm:


2019-11-12 18:03:32.487 DEBUG 19592 --- [o-auto-1-exec-3] o.s.s.w.u.m.AndRequestMatcher            : All requestMatchers returned true
2019-11-12 18:03:32.487 DEBUG 19592 --- [o-auto-1-exec-3] o.s.s.w.DefaultRedirectStrategy          : Redirecting to 'http://localhost:51599/login'
...
2019-11-12 18:03:32.513 DEBUG 19592 --- [o-auto-1-exec-4] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed
2019-11-12 18:03:32.514 ERROR 19592 --- [cTaskExecutor-1] o.s.w.s.s.c.DefaultTransportRequest      : No more fallback transports after TransportRequest[url=ws://localhost:51599/app/299/6928331c14b94beba4315294661970c3/websocket]
javax.websocket.DeploymentException: The HTTP response from the server [200] did not permit the HTTP upgrade to WebSocket
    at org.apache.tomcat.websocket.WsWebSocketContainer.connectToServerRecursive(WsWebSocketContainer.java:437) ~[tomcat-embed-websocket-9.0.27.jar:9.0.27]
    at org.apache.tomcat.websocket.WsWebSocketContainer.connectToServerRecursive(WsWebSocketContainer.java:395) ~[tomcat-embed-websocket-9.0.27.jar:9.0.27]
    at org.apache.tomcat.websocket.WsWebSocketContainer.connectToServer(WsWebSocketContainer.java:197) ~[tomcat-embed-websocket-9.0.27.jar:9.0.27]
    at org.springframework.web.socket.client.standard.StandardWebSocketClient.lambda$doHandshakeInternal$0(StandardWebSocketClient.java:151) ~[spring-websocket-5.2.0.RELEASE.jar:5.2.0.RELEASE]
    at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:264) [?:?]
    at java.util.concurrent.FutureTask.run(FutureTask.java) [?:?]
    at java.lang.Thread.run(Thread.java:844) [?:?]

While nowhere is documented how to authenticate a websocket client, other then with session header, I tried to manually login, retrieve the session cookie to set it in the WS Connection / Handshake Headers, with no luck, because it seems the MockMvc does not store or set any cookies in the headers.


@Test
public void test() throws Exception {
    MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(wac)
        //.addFilters(springSecurityFilterChain) // same result
        .apply(springSecurity())
        .build();

    MvcResult response = mockMvc.perform(formLogin("/perform_login")
        .user("email", "user@test.com")
        .password("pw", "test123"))
        .andDo(print())
        .andExpect(status().isOk())
        .andExpect(cookie().exists("JSESSIONID")) // Test is failing here
        .andReturn();
}

Output from request print:


MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /perform_login
       Parameters = {email=[user@test.com], pw=[test123], _csrf=[933cd6ad-0c81-4539-82a5-b184babcad84]}
          Headers = [Accept:"application/x-www-form-urlencoded"]
             Body = <no character encoding set>
    Session Attrs = {userid=88, SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@c08a487f: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@c08a487f: Principal: com.package.UserDetails@7ca802e3; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: FULL_AUTH, username=user@test.com}

Handler:
             Type = null

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [X-Redirect:"/", Content-Type:"application/json", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = application/json
             Body = {"code":200, "status":"ok"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

The Application works like a charm, Login is working and websocket is working too.
The normal Login responds with a cookie and everything is fine.

Questions

  1. How can i authenticate the websocket client in an JUnit Test scenario with a given user in an elegant (spring) way?
  2. How can i get the session id from MockMVC result to successfully connect with websocket (by inserting cookie in handshake/upgrade headers)?
  3. Is there an other way testing secured websocket?

Known related SO-Topics:

Same exception with Websocket connection (but no answers)
Same behaviour with MockMVC-Cookies (none of the answers worked)

Sunchezz
  • 740
  • 6
  • 21

0 Answers0