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
- How can i authenticate the websocket client in an JUnit Test scenario with a given user in an elegant (spring) way?
- How can i get the session id from MockMVC result to successfully connect with websocket (by inserting cookie in handshake/upgrade headers)?
- 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)