JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket

ghz 1years ago ⋅ 5925 views

Question

Background

I am in the process of setting up a RESTful web application using Spring Boot (1.3.0.BUILD-SNAPSHOT) that includes a STOMP/SockJS WebSocket, which I intend to consume from an iOS app as well as web browsers. I want to use JSON Web Tokens (JWT) to secure the REST requests and the WebSocket interface but I’m having difficulty with the latter.

The app is secured with Spring Security:-

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    public WebSecurityConfiguration() {
        super(true);
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("steve").password("steve").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .exceptionHandling().and()
            .anonymous().and()
            .servletApi().and()
            .headers().cacheControl().and().and()

            // Relax CSRF on the WebSocket due to needing direct access from apps
            .csrf().ignoringAntMatchers("/ws/**").and()

            .authorizeRequests()

            //allow anonymous resource requests
            .antMatchers("/", "/index.html").permitAll()
            .antMatchers("/resources/**").permitAll()

            //allow anonymous POSTs to JWT
            .antMatchers(HttpMethod.POST, "/rest/jwt/token").permitAll()

            // Allow anonymous access to websocket 
            .antMatchers("/ws/**").permitAll()

            //all other request need to be authenticated
            .anyRequest().hasRole("USER").and()

            // Custom authentication on requests to /rest/jwt/token
            .addFilterBefore(new JWTLoginFilter("/rest/jwt/token", authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)

            // Custom JWT based authentication
            .addFilterBefore(new JWTTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }

}

The WebSocket configuration is standard:-

@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }

}

I also have a subclass of AbstractSecurityWebSocketMessageBrokerConfigurer to secure the WebSocket:-

@Configuration
public class WebSocketSecurityConfiguration extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages.anyMessage().hasRole("USER");
    }

    @Override
    protected boolean sameOriginDisabled() {
        // We need to access this directly from apps, so can't do cross-site checks
        return true;
    }

}

There is also a couple of @RestController annotated classes to handle various bits of functionality and these are secured successfully via the JWTTokenFilter registered in my WebSecurityConfiguration class.

Problem

However I can't seem to get the WebSocket to be secured with JWT. I am using SockJS 1.1.0 and [STOMP 1.7.1](https://raw.githubusercontent.com/jmesnil/stomp- websocket/master/lib/stomp.js) in the browser and can't figure out how to pass the token. It [would appear that](https://github.com/sockjs/sockjs- client/issues/196) SockJS does not allow parameters to be sent with the initial /info and/or handshake requests.

The [Spring Security for WebSockets documentation states](http://docs.spring.io/autorepo/docs/spring- security/4.0.x/reference/html/websocket.html) that the AbstractSecurityWebSocketMessageBrokerConfigurer ensures that:

Any inbound CONNECT message requires a valid CSRF token to enforce Same Origin Policy

Which seems to imply that the initial handshake should be unsecured and authentication invoked at the point of receiving a STOMP CONNECT message. Unfortunately I can't seem to find any information with regards to implementing this. Additionally this approach would require additional logic to disconnect a rogue client that opens a WebSocket connection and never sends a STOMP CONNECT.

Being (very) new to Spring I'm also not sure if or how Spring Sessions fits into this. While the documentation is very detailed there doesn't appear to a nice and simple (aka idiots) guide to how the various components fit together / interact with each other.

Question

How do I go about securing the SockJS WebSocket by providing a JSON Web Token, preferably at the point of handshake (is it even possible)?


Answer

Seems like support for a query string was added to the SockJS client, see https://github.com/sockjs/sockjs-client/issues/72.