1. Introduction

In a previous article, we showed how to add WebSockets to a Spring MVC project.

Here, we’ll describe how to add security to Spring WebSockets in Spring MVC. Before continuing, make sure you already have basic Spring MVC Security coverage in place – if not, check out this article.

2. Maven Dependencies

There are two main groups of Maven dependencies we need for our WebSocket implementation.

First, let’s specify the overarching versions of the Spring Framework and Spring Security that we will be using:

<properties>
    <spring.version>6.0.12</spring.version>
    <spring-security.version>6.1.5</spring-security.version>
    <spring-security-messaging.version>6.0.2</spring-security-messaging.version>
</properties>

Second, let’s add the core Spring MVC and Spring Security libraries required to implement basic authentication and authorization:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>${spring-security.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>${spring-security.version}</version>
</dependency>

The latest versions of spring-core, spring-web, spring-webmvc, spring-security-web, spring-security-config can be found on Maven Central.

Lastly, let’s add the required dependencies:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-websocket</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-messaging</artifactId>
    <version>${spring.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-messaging</artifactId>
    <version>${spring-security-messaging.version}</version>
</dependency>

You can find the latest version of spring-websocket, spring-messaging, and spring-security-messaging on Maven Central.

3. Basic WebSocket Security

WebSocket-specific security using the spring-security-messaging library centers on the AbstractSecurityWebSocketMessageBrokerConfigurer class and its implementation within your project:

@Configuration
public class SocketSecurityConfig 
  extends AbstractSecurityWebSocketMessageBrokerConfigurer {
      //...
}

The AbstractSecurityWebSocketMessageBrokerConfigurer class provides additional security coverage provided by WebSecurityConfigurerAdapter.

The spring-security-messaging library is not the only way to implement security for WebSockets. If we stick with the ordinary spring-websocket library, we can implement the WebSocketConfigurer interface and attach security interceptors to our socket handlers.

Since we are using the spring-security-messaging library, we will use the AbstractSecurityWebSocketMessageBrokerConfigurer approach.

3.1. Implementing configureInbound()

The implementation of configureInbound() is the most important step in configuring your AbstractSecurityWebSocketMessageBrokerConfigurer subclass:

@Override 
protected void configureInbound(
  MessageSecurityMetadataSourceRegistry messages) { 
    messages
      .simpDestMatchers("/secured/**").authenticated()
      .anyMessage().authenticated(); 
}

Whereas the WebSecurityConfigurerAdapter lets you specify various application-wide authorization requirements for different routes, AbstractSecurityWebSocketMessageBrokerConfigurer allows you to specify the specific authorization requirements for socket destinations.

3.2. Type and Destination Matching

MessageSecurityMetadataSourceRegistry allows us to specify security constraints like paths, user roles, and which messages are allowed.

Type matchers constrain which SimpMessageType are allowed and in what way**:**

.simpTypeMatchers(CONNECT, UNSUBSCRIBE, DISCONNECT).permitAll()

Destination matchers constrain which endpoint patterns are accessible and in what way**:**

.simpDestMatchers("/app/**").hasRole("ADMIN")

Subscribe destination matchers map a List of SimpDestinationMessageMatcher instances that match on SimpMessageType.SUBSCRIBE:

.simpSubscribeDestMatchers("/topic/**").authenticated()

Here is the complete list of all available methods for type and destination matching.

4. Securing Socket Routes

Now that we’ve been introduced to basic socket security and type matching configuration, we can combine socket security, views, STOMP (a text-messaging protocol), message brokers, and socket controllers to enable secure WebSockets within our Spring MVC application.

First, let’s set up our socket views and controllers for basic Spring Security coverage:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
@EnableWebSecurity
@ComponentScan("com.baeldung.springsecuredsockets")
public class SecurityConfig {

    /**
     * Order of precedence is very important.
     * <p>
     * Matching occurs from top to bottom - so, the topmost match succeeds first.
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry ->
                        authorizationManagerRequestMatcherRegistry
                                .requestMatchers("/", "/index", "/authenticate").permitAll()
                                .requestMatchers("/secured/**/**", "/secured/**/**/**", "/secured/socket", "/secured/success").authenticated()
                                .anyRequest().authenticated())
            .formLogin(httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer.loginPage("/login").permitAll()
                    .usernameParameter("username")
                    .passwordParameter("password")
                    .loginProcessingUrl("/authenticate")
                    .successHandler(loginSuccessHandler())
                    .failureUrl("/denied").permitAll())
            //...
    }
}

Second, let’s set up the actual message destination with authentication requirements:

@Configuration
public class SocketSecurityConfig 
  extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
          .simpDestMatchers("/secured/**").authenticated()
          .anyMessage().authenticated();
    }   
}

Now, in our WebSocketMessageBrokerConfigurer, we can register the actual message and STOMP endpoints:

@Configuration
@EnableWebSocketMessageBroker
public class SocketBrokerConfig 
  implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/secured/history");
        config.setApplicationDestinationPrefixes("/spring-security-mvc-socket");
    }

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

Let’s define an example socket controller and endpoint that we provided security coverage for above:

@Controller
public class SocketController {
 
    @MessageMapping("/secured/chat")
    @SendTo("/secured/history")
    public OutputMessage send(Message msg) throws Exception {
        return new OutputMessage(
           msg.getFrom(),
           msg.getText(), 
           new SimpleDateFormat("HH:mm").format(new Date())); 
    }
}

5. Same Origin Policy

The Same Origin Policy requires that all interactions with an endpoint must come from the same domain where the interaction was initiated.

For example, suppose your WebSockets implementation is hosted at foo.com, and you are enforcing same origin policy. If a user connects to your client hosted at foo.com and then opens another browser to bar.com, then bar.com will not have access to your WebSocket implementation.

5.1. Overriding the Same Origin Policy

Spring WebSockets enforce the Same Origin Policy out of the box, while ordinary WebSockets do not.

In fact, Spring Security requires a CSRF (Cross Site Request Forgery) token for any valid CONNECT message type:

@Controller
public class CsrfTokenController {
    @GetMapping("/csrf")
    public @ResponseBody String getCsrfToken(HttpServletRequest request) {
        CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
        return csrf.getToken();
    }
}

By calling the endpoint at /csrf, a client can acquire the token and authenticate through the CSRF security layer.

However, the Same Origin Policy for Spring can be overridden by adding the following configuration to your AbstractSecurityWebSocketMessageBrokerConfigurer:

@Override
protected boolean sameOriginDisabled() {
    return true;
}

5.2. STOMP, SockJS Support, and Frame Options

It is common to use STOMP along with SockJS to implement client-side support for Spring WebSockets.

SockJS is configured to disallow transports through HTML iframe elements by default. This is to prevent the threat of clickjacking.

However, there are certain use-cases where allowing iframes to leverage SockJS transports can be beneficial. To do so, you can create SecurityFilterChain bean:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) 
  throws Exception {
    http
      .csrf(AbstractHttpConfigurer::disable)
        //...
      .headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
      .authorizeHttpRequests(Customizer.withDefaults());
    return http.build();
}

Note that in this example, we follow the Same Origin Policy despite allowing transports through iframes.

6. Oauth2 Coverage

Oauth2-specific support for Spring WebSockets is made possible by implementing Oauth2 security coverage in addition to — and by extending — your standard WebSecurityConfigurerAdapter coverage*.* Here’s an example of how to implement Oauth2.

To authenticate and gain access to a WebSocket endpoint, you can pass an Oauth2 access_token into a query parameter when connecting from your client to your back-end WebSocket.

Here’s an example demonstrating that concept using SockJS and STOMP:

var endpoint = '/ws/?access_token=' + auth.access_token;
var socket = new SockJS(endpoint);
var stompClient = Stomp.over(socket);

7. Conclusion

In this brief tutorial, we have shown how to add security to Spring WebSockets. Take a look at Spring’s WebSocket and WebSocket Security reference documentation to if you are looking to learn more about this integration.

As always, check our Github project for examples used in this article.