Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Webflux Websocket Security - Basic Authentication

PROBLEM: I am not getting Spring Security with Websockets to work in a Webflux project.

NOTE: I am using Kotlin instead of Java.

DEPENDENCIES:

  • Spring Boot 2.0.0

  • Spring Security 5.0.3

  • Spring WebFlux 5.0.4

IMPORTANT UPDATE: I have raised a Spring Issue bug (March 30) here and one of the Spring security maintainers said its NOT SUPPORTED but they can add it for Spring Security 5.1.0 M2.

LINK: Add WebFlux WebSocket Support #5188

Webflux Security Configuration

@EnableWebFluxSecurity
class SecurityConfig
{
    @Bean
    fun configure(http: ServerHttpSecurity): SecurityWebFilterChain
    {

        return http.authorizeExchange()
            .pathMatchers("/").permitAll()
            .anyExchange().authenticated()
            .and().httpBasic()
            .and().formLogin().disable().csrf().disable()
            .build()
    }

    @Bean
    fun userDetailsService(): MapReactiveUserDetailsService
    {
        val user = User.withDefaultPasswordEncoder()
            .username("user")
            .password("pass")
            .roles("USER")
            .build()

        return MapReactiveUserDetailsService(user)
    }
}

Webflux Websocket Configuration

@Configuration
class ReactiveWebSocketConfiguration
{
    @Bean
    fun webSocketMapping(handler: WebSocketHandler): HandlerMapping
    {
        val map = mapOf(Pair("/event", handler))
        val mapping = SimpleUrlHandlerMapping()
        mapping.order = -1
        mapping.urlMap = map
        return mapping
    }

    @Bean
    fun handlerAdapter() = WebSocketHandlerAdapter()

    @Bean
    fun websocketHandler() = WebSocketHandler { session ->

        // Should print authenticated principal BUT does show NULL
        println("${session.handshakeInfo.principal.block()}")

        // Just for testing we send hello world to the client
        session.send(Mono.just(session.textMessage("hello world")))
    }
}

Client Code

// Lets create a websocket and pass Basic Auth to it
new WebSocket("ws://user:pass@localhost:8000/event");
// ...

Obserservations

  1. In the websocket handler the principal shows null

  2. The client can connect without being authenticated. If I do WebSocket("ws://localhost:8000/event") without the Basic Auth it stills works! So Spring Security does not authenticate anything.

What I am missing? What I do wrong?

like image 826
Dachstein Avatar asked Mar 28 '18 22:03

Dachstein


People also ask

Does WebFlux use WebSockets?

Spring WebFlux can also be integrated with WebSockets to provide notifications that clients can listen to. Combining the two is a powerful way to provide real-time data streaming to JavaScript or mobile clients.


1 Answers

I could advise you to implement your own authentication mechanism instead of exploiting Spring Security.

When WebSocket connection is about to establish it uses handshake mechanism accompanied by an UPGRADE request. Base on that, our idea would be to use our own handler for the request and perform authentication there.

Fortunately, Spring Boot has RequestUpgradeStrategy for such purpose. On top of that, based on the application server what you use, Spring provides a default implementation of those strategies. As I use Netty bellow the class would be ReactorNettyRequestUpgradeStrategy.

Here is the suggested prototype:

/**
 * Based on {@link ReactorNettyRequestUpgradeStrategy}
 */
@Slf4j
@Component
public class BasicAuthRequestUpgradeStrategy implements RequestUpgradeStrategy {

    private int maxFramePayloadLength = NettyWebSocketSessionSupport.DEFAULT_FRAME_MAX_SIZE;

    private final AuthenticationService service;

    public BasicAuthRequestUpgradeStrategy(AuthenticationService service) {
        this.service = service;
    }

    @Override
    public Mono<Void> upgrade(ServerWebExchange exchange, //
                              WebSocketHandler handler, //
                              @Nullable String subProtocol, //
                              Supplier<HandshakeInfo> handshakeInfoFactory) {

        ServerHttpResponse response = exchange.getResponse();
        HttpServerResponse reactorResponse = getNativeResponse(response);
        HandshakeInfo handshakeInfo = handshakeInfoFactory.get();
        NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory) response.bufferFactory();

        String originHeader = handshakeInfo.getHeaders()
                                           .getOrigin();// you will get ws://user:pass@localhost:8080

        return service.authenticate(originHeader)//returns Mono<Boolean>
                      .filter(Boolean::booleanValue)// filter the result
                      .doOnNext(a -> log.info("AUTHORIZED"))
                      .flatMap(a -> reactorResponse.sendWebsocket(subProtocol, this.maxFramePayloadLength, (in, out) -> {

                          ReactorNettyWebSocketSession session = //
                                  new ReactorNettyWebSocketSession(in, out, handshakeInfo, bufferFactory, this.maxFramePayloadLength);

                          return handler.handle(session);
                      }))
                      .switchIfEmpty(Mono.just("UNATHORIZED")
                                         .doOnNext(log::info)
                                         .then());

    }

    private static HttpServerResponse getNativeResponse(ServerHttpResponse response) {
        if (response instanceof AbstractServerHttpResponse) {
            return ((AbstractServerHttpResponse) response).getNativeResponse();
        } else if (response instanceof ServerHttpResponseDecorator) {
            return getNativeResponse(((ServerHttpResponseDecorator) response).getDelegate());
        } else {
            throw new IllegalArgumentException("Couldn't find native response in " + response.getClass()
                                                                                             .getName());
        }
    }
}

Moreover, if you do not have crucial logical dependencies onto Spring Security in the project such as complex ACL logic, then I advise you to get rid of it and even do not use it at all.

The reason for that is that I see Spring Security as a violator of the reactive approach due to its, I would say, MVC legacy mindset. It entangles your application with tons of extra configurations and "not-on-the-surface" tunings and forces engineers to maintain those configurations, making them more and more complex. In most cases, things could be implemented very smoothly without touching Spring Security at all. Just create a component and use it in a proper way.

Hope it helps.

like image 103
Serhii Povísenko Avatar answered Oct 09 '22 23:10

Serhii Povísenko