Hi,
I'm currently migrating an application to Spring Boot 3.x. This application declare a rest controller and a websocket endpoint authenticated via basic auth. But the basic auth does not work anymore on the websocket endpoint.
Sample application: https://github.com/rcosne/ws-test
RestController: http://localhost:8080/test Websocket endpoint: ws://localhost:8080/wstest
It seems that the whole security filter chain is skipped in this case. I've tried to declare a customer filter via @Component, and via a JettyServerCustomizer, in both case, the filter is applied in the rest controller, but not in the websocket endpoint.
I've also tested with Tomcat, then the basic auth works with the websocket.
Best Regards, Rémy
Comment From: wilkinsona
Thanks for the sample. The difference in behaviour is due to different ordering of the filters that handle the initial upgrade request. With Tomcat, this filter is at the end of the chain. Crucially, this means that Spring Security's filter runs before the upgrade request is handled and can require basic authentication. With Jetty, this filter is at the start of the chain and its handling of the upgrade request is such that the rest of the chain isn't called. Crucially, this means that Spring Security's filter doesn't get a chance to run.
With both Tomcat and Jetty, the filter that handles upgrades is registered last so it should be at the end of the chain. However, that's not the case with Jetty because Jetty forces it to the start of the chain. Unfortunately, this clashes with a filter-based security framework, such as Spring Security, preventing it from securing upgrade requests. This appears to have been a change in Jetty 10.
We could possibly override this behavior by registering Jetty's WebSocketUpgradeFilter ourselves rather than relying on org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter.ensureFilter(ServletContext) but that feels potentially brittle. It also wouldn't help other Jetty users as org.eclipse.jetty.websocket.jakarta.server.config.JakartaWebSocketServletContainerInitializer calls ensureFilter.
For these reasons I think it would be better to address this in Jetty itself. To that end, please open a Jetty issue for this so that they can investigate. Looking at https://github.com/eclipse/jetty.project/pull/5648 (which drove the change to the filter's order) they did consider filters like Spring Security but they talk about web.xml which isn't applicable in a Spring Boot app using embedded Jetty.
If a general fix in Jetty isn't possible, we can re-open the issue and consider a solution that's specific to Spring Boot.
Comment From: joakime
The reason for the websocket upgrade filter being first is simply because there are thousands of existing Filters out in the wild that modify the request / response, wrap the HttpServletRequest and/or HttpServletResponse, or provide overrides of the HttpServletRequest.getInputStream() or HttpServletResponse.getOutputStream() that all break when websocket is in the mix.
The decision to put upgrade at the start is intentional to not break the thousands of webapps that are not websocket aware.
Also, (putting on my jakarta websocket hat), there is zero requirement for a server that supports jakarta websocket to support that upgrade via a filter.
Some actual implementations of jakarta.websocket server upgrade that exist in the wild.
- No servlets involved at all, the server doesn't even support servlets or has a servlet jar.
- Upgrade done outside of a
ServletContext(and still supportsjakarta.websocket.server.ServerApplicationConfig) - Upgrade at the same layer as Servlet Security, and Servlet Session handling (which is before the Filter Chain is evaluated)
Assuming that the jakarta.websocket upgrade will be done in a filter is a poor assumption.
(putting my jetty hat back on)
Jetty at one point had the javax.websocket upgrade at the steps before the Filter Chain is evaluated (after Servlet Security, and Servlet Session handling).
That upgrade style doesn't exist in Jetty 10 / 11 / 12 at this point in time.
A workaround for Spring Boot is to add the existing WebSocketUpgradeFilter itself (no need to override / extend the default one), before adding endpoints, or accessing the websocket ServerContainer.
Just make sure to define it with RequestDispatcher.REQUEST only, on a url-mapping of /*, with async-supported set to true.
Comment From: joakime
@wilkinsona would you like PR that introduces a special ServletContainerInitializer for spring-boot that puts the WebSocketUpgradeFilter where you want it?
I suggest a ServletContainerInitializer as that can be used with discovery tooling to support automatic additions of jakarta websocket endpoints.
aka.
import jakarta.websocket.Endpoint;
import jakarta.websocket.server.ServerApplicationConfig;
import jakarta.websocket.server.ServerContainer;
import jakarta.websocket.server.ServerEndpoint;
import jakarta.websocket.server.ServerEndpointConfig;
@HandlesTypes({ServerApplicationConfig.class, ServerEndpoint.class, Endpoint.class})
public class SpringBootJettyWebSocketUpgradeInitializer implements ServletContainerInitializer
Comment From: wilkinsona
Thanks, @joakime. Sounds like we need to try to do something in Boot to address this.
would you like PR that introduces a special
ServletContainerInitializer
Thanks for the offer. Boot's doesn't honour the ServletContainertInitializer contract but I think we can achieve a similar end result using a FilterRegistrationBean.
@rcosne, you can work around your problem with the following addition to your app:
@Bean
FilterRegistrationBean<WebSocketUpgradeFilter> webSocketUpgradeFilter() {
FilterRegistrationBean<WebSocketUpgradeFilter> registration = new FilterRegistrationBean<>(new WebSocketUpgradeFilter());
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setName(WebSocketUpgradeFilter.class.getName());
registration.setOrder(Ordered.LOWEST_PRECEDENCE);
registration.setUrlPatterns(List.of("/*"));
return registration;
}
We can probably add this bean directly to Boot but, given what @joakime has said above, I am a little wary of the potential for a regression. If someone's using Jetty and WebSockets without Spring Security they may be relying on the current filter ordering.
Comment From: wilkinsona
As suspected, Spring Boot 3.0.x (I tested 3.0.10) is also affected and 2.7.x (I tested 2.7.15) is not affected.
Comment From: rcosne
Hi @wilkinsona, the workaround works ! Thank you.
Comment From: joakime
@wilkinsona nice.
BTW, If you have a user of Jetty 9, there's an extra requirement for that FilterRegistrationBean setup.
In Jetty 9, there is also a ServletContext attribute requirement for it to work. (thankfully we got rid of this requirement in Jetty 10) It would look something like this ...
@Bean
FilterRegistrationBean<WebSocketUpgradeFilter> webSocketUpgradeFilter() {
WebSocketUpgradeFilter websocketFilter = new WebSocketUpgradeFilter();
getServletContext().setAttribute(WebSocketUpgradeFilter.ATTR_KEY, websocketFilter);
FilterRegistrationBean<WebSocketUpgradeFilter> registration = new FilterRegistrationBean<>(websocketFilter);
registration.setAsyncSupported(true);
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setName(WebSocketUpgradeFilter.class.getName());
registration.setOrder(Ordered.LOWEST_PRECEDENCE);
registration.setUrlPatterns(List.of("/*"));
return registration;
}
Without this ServletContext attribute in Jetty 9, there would be 2 WebSocketUpgradeFilters, causing your initialization to either fail, or in your FilterRegistrationBean to add a duplicate WebSocketUpgradeFilter to a place/position that doesn't matter (as the default one is still in its early location)
Comment From: wilkinsona
There's no need for the registration bean in Spring Boot 2.7 with Jetty 9 as the filter ordering's fine there. Regardless, thanks for sharing the tip about the attribute as it may prove useful for someone in the future who does need to tweak the filter ordering.
Comment From: joakime
With Jetty 9, the default behavior on standalone Jetty, or an embedded Jetty user that relies on the default WebApp / WebAppContext Configurations, is ...
- The
org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializeris responsible for adding theWebSocketUpgradeFilter - The nature of Servlet Initialization ordering (between webapp / container / server / and various descriptors) means this
WebSocketUpgradeFilterwill be first in line on theFilterChain(just like Jetty 10/11/12)
Comment From: wilkinsona
While we use WebSocketServerContainerInitializer, we do so in an unusual way. As mentioned above, Boot doesn't support the ServletContainerInitializer contract so WebSocketServerContainerInitializer is triggered through an explicit call to initialize. This happens after other filters have already been registered, allowing Spring Security to secure upgrade requests and also aligning the behavior with Tomcat.