How to Overcome WebSocket’s Authentication and Authorization Issues (with Stomp.js and Spring Boot)

A popular JavaScript library, Stomp.js is one of developers’ favorites since it provides a simple and user-friendly  interface for WebSockets. Yet, despite its ease of use, you probably went through many struggles so you can properly implement Stomp authentication and authorization with Spring-Security. Specifically, you noticed that Stomp.js doesn’t set the necessary HTTP WebSocket handshake headers that Spring Security requires for automatic authentication. 

Below, you’ll find all the details of an easy workaround to this issue, so you can keep using your favorite tool to build interactive web applications.

How to Solve the Error with WebSockets Handshake

In the context of using Stomp.js with Spring Security, sending the authorization bearer token on connect is a crucial step to ensure secure communication between the client and the server. When a client connects to a WebSocket endpoint, the server must verify the client’s identity before allowing access to the protected resources. 

Spring Security provides an authentication mechanism that allows clients to authenticate using the OAuth2 protocol and obtain an access token, which can be used to authorize subsequent requests.

To send the authorization bearer token on connect with Stomp.js, you need to set the appropriate headers in the STOMP CONNECT frame, specifically, the “Authorization” header to “Bearer <access_token>”. 

The “<access_token>” placeholder should be replaced with the actual access token obtained during the authentication process. 
The CONNECT frame can be sent using the “stompClient.connect(headers, connectCallback)” method, where “headers” is an object containing the headers to be sent and “connectCallback” is a function that will be called when the connection is established.

var headers = {
'Authorization': 'Bearer' + ACCESS_TOKEN
};
client.connect(headers, connectCallback);

Secure WebSockets Endpoints

When working with WebSockets and Spring Security, it’s important to understand that the authorization process won’t happen on the HTTP negotiation endpoint. This means that you have to adopt another strategy to reach the point where you have applied security restrictions for all the sensitive information exposed through WebSockets.

What you need to do in the first place on the server side is to make sure that you allow any

connection attempt to sockets, open to any client through HTTP negotiation endpoint (handshake). 

For this, set up the required security configuration (assuming that you already have security in place for our HTTP incoming requests).

@Configuration
@Order(1)
public class WebSecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(STATELESS)
.and()
.httpBasic().disable()
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) // Your custom
authorization for HTTP routes that your server exposes
.authorizeHttpRequests(auth -> auth.antMatchers(GET, "/stomp").permitAll()); //
HTTP handshake endpoint
return http.build();
}
}

Server’s WebSocket configuration, for lightweight understanding purposes will look something like the code below:

@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
// Handshake endpoint
registry.addEndpoint("stomp");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// We have to register an authorizationInterceptor that will be responsible for
// validating access-token and user access rights for any incoming CONNECTION
// attempts.
registration.interceptors(authorizationInterceptor);
}
@Override
public void configureMessageBroker(final MessageBrokerRegistry registry) {
// Topics that clients can subscribe to
registry.enableSimpleBroker("");
// Any messages with below destination prefixes will be sent to @MessageMappings
controllers
registry.setApplicationDestinationPrefixes("");
// Configure the prefix used to identify user destinations.
// User destinations provide the ability for a user to subscribe
// to queue names unique to their session as well as for others to send messages to
those unique,
// user-specific queues.
registry.setUserDestinationPrefix("");
}
}

*Note: this is just a basic WebSocket configuration, and you can feel free to extend this configuration based on your requirements anytime; for now,  just  keep it simple, so you can fully understand how the goal of this topic can be achieved.

As stated within official docs, “Spring Security secures only the clientInboundChannel. Spring Security does not attempt to secure the clientOutboundChannel. The most important reason for this is performance. 

For every message that goes in, typically, many more go out. Instead of securing the outbound messages, it’s better that you secure the subscription to the endpoints.” (Spring Security WebSocket ref). 

For that reason, you can register within your WebScoketConfiguration class a custom interceptor that will be responsible for authorizing any incoming attempts for topics subscription.

How to Control the Order of Configuration in Spring Boot

One tiny, yet very important not to be missed detail on the above configuration that you implemented is the presence of “@Order” annotation for the configuration class.

@Order annotation is used to specify the order in which the components should be processed. It’s commonly used to control the order of execution of multiple interceptors or filters, where the order can affect the outcome.

The @Order annotation can be applied to a class or a method and takes a single integer value as its argument. The lower the value, the higher the priority, with the default being Ordered.LOWEST_PRECEDENCE. 

Therefore, a component with an @Order value of 1 will be processed before a component with an @Order value of 2.

It’s important to note that the @Order annotation only affects the order in which the components are processed within the same phase of the application context. If you have components that belong to different phases, you should use the appropriate annotations to control their order, such as @PostConstruct and @PreDestroy.

Overall, the @Order annotation is a simple but powerful way to control the processing order of components in a Spring Boot application.

The Role of Spring Boot Interceptors

Now that we have introduced the definition of “interceptor” both into our WebSocket’s configuration class, let’s look first at the interceptor’s definition and understand its main goal. 

The interceptor’s main goal is to intercept and modify incoming HTTP requests and outgoing HTTP responses. Interceptors provide a way to implement cross-cutting concerns such as logging, auditing, security, and performance monitoring, without duplicating code across multiple controllers or views

By intercepting requests and responses, interceptors can modify the behavior of the application and add additional functionality. For example, an interceptor can log the details of the incoming request before the controller handles it or log the details of the response before it’s sent back to the client.

You can also use interceptors to perform pre- and post-processing of requests and responses. For example, an interceptor can log the details of the incoming request before it is handled by the controller, or log the details of the response before it is sent back to the client. 

Overall, an interceptor’s main goal is to provide a way to implement cross-cutting concerns in a modular and reusable way, allowing developers to add functionality to their applications without duplicating code or cluttering their controllers or views with extra logic.

Your custom defined interceptor should override the “preSendMethod” like in the following example. Of course, there are other methods within the ChannelInterceptor class that can be overridden, like:

  • postSend
  • afterSendCompletion 
  • preReceive
  • postReceive
  • afterReceiveCompletion

But for this goal, the focus should be only on the preSend method.

@Slf4j
@RequiredArgsConstructor
public class AuthorizationSocketInterceptor implements ChannelInterceptor {
private final JwtDecoder jwtDecoder;
@Override
public Message<?> preSend(final Message<?> message, final MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
//Authenticate user on CONNECT
if (nonNull(accessor) && StompCommand.CONNECT.equals(accessor.getCommand())) {
//Extract JWT token from header, validate it and extract user authorities
val authHeader = accessor.getFirstNativeHeader("Authorization");
if (isNull(authHeader) || !authHeader.startsWith("Bearer" + " ")) {
// If there is no token present then we should interrupt handshake process and throw an
AccessDeniedException
throw new AccessDeniedException(WebSocketSecurityConfig.WS_UNAUTHORIZED_MESSAGE);
}
val token = authHeader.substring("Bearer".length() + 1);
Jwt jwt;
try {
//Validate JWT token with any resource server
jwt = jwtDecoder.decode(token);
} catch (JwtException ex) {
//In case the JWT token is expired or cannot be decoded, an AccessDeniedException should be
thrown
log.warn(ex.getMessage());
throw new AccessDeniedException(WebSocketSecurityConfig.WS_UNAUTHORIZED_MESSAGE);
}
JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwt, getUserAuthorities(jwt));
accessor.setUser(authentication);
}
return message;
}
}

Apply WebSocket Authorization Rules for the Server

Only one step left to be covered before we can consider our implementation done and our WebSocket communication with any client secure. Now that you have everything in place (i.e: WebSockets configuration, HTTP security configuration, AuthorizationSocketInterceptor), you still need to apply authorization rules for our server. 

For that, you’d have to create a WebScoketAuthorizationSecurityConfiguration and define your rules for each topic configured inside WebSocketConfiguration. 

For more details on how to configure and extend your authorization rules, check this introductory reference about security and WebSockets.

Here’s an example on how to follow some basic configuration rules for your WebSocketAuthorizationSecurityConfiguration class.

@Configuration
public class WebSocketAuthorizationSecurityConfig extends
AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages)
{
// Customize your authorization here
messages
.nullDestMatcher().authenticated()
.simpDestMatchers(“/topic/system/notifications”).hasRole(“ADMIN”)
.anyMessage().authenticated();
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}

Conclusion

Hope you now have a clearer view about maintaining a spring session during a WebSocket connection through handshake interceptors. You’ll be able to track user sessions during every WebSocket request and also track client activities from the server.

Additionally,  you can provide extra security even after connecting the server through the WebSocket protocol.