Authentication in the WebSocket protocol is not as straightforward as some other communication protocols. When creating a connection from JavaScript, customizing the headers in the WebSocket handshake request is not an option. This leaves us unable to send authentication/authorization information securely in the headers of the request. However, we can still pass the information via a query-string parameter. Since URLs can be logged and captured even when securing communication with SSL, it would be unwise to pass internal authentication tokens that may contain sensitive information. What we will demonstrate here is an implementation of a pattern that can hurdle these obstacles by utilizing an HTTP endpoint for authentication/authorization using Java and Spring Boot.

Note : This blog does not go into detail on securing communications via SSL. To ensure proper security, SSL communication should be enabled between the client and server.

Authentication Flow

websocket-auth-flow.png
websocket-auth-flow.png
  1. Requests to authenticate are made to the HTTP endpoint /authenticate/token with the internal authentication token securely passed in the header of the request. The server generates a temporary external authentication token, stores it in the Authentication Cache, and returns it to the client.

  2. The client makes a WebSocket handshake request with the external authentication token passed as a query-string parameter in the handshake endpoint URL. The server checks the cache to see if the external authentication token is valid. If valid, the handshake is established and the HTTP upgrade occurs to the WebSocket protocol.

  3. The client has now been authenticated and bidirectional communication can now occur.

Authentication Controller

@GetMapping("/token")
  @ResponseStatus(HttpStatus.OK)
  public UUID getToken() {
    UUID websocketAuthToken = UUID.randomUUID();

    WebSocketAuthInfo webSocketAuthInfo =
      new WebSocketAuthInfo(websocketAuthToken);

    authCache.put(websocketAuthToken, webSocketAuthInfo);

    return websocketAuthToken;
  }

Here we can see the HTTP endpoint for requesting a temporary external authentication token for the WebSocket handshake. A random UUID is generated and stored in the cache and then returned back to the client.

HTTP Endpoint Configuration

@Override
  public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new HttpAuthenticationInterceptor())
      .addPathPatterns("/authentication/token");
  }

To ensure that requests to the authentication token endpoint are secure, we configure an interceptor to validate that the proper authentication/authorization header has been provided. The HttpAuthenticationInterceptor class contains the logic to do this. Any requests to the endpoint /authenticate/token will first go through the interceptor.

WebSocket Configuration

@Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry
      .addEndpoint("/websocket/connect")
      .addInterceptors(webSocketHandshakeAuthInterceptor)
      .setAllowedOrigins("*")
      .withSockJS();
  }

Here we can see that we’ve added an interceptor on the WebSocket handshake endpoint. This interceptor is responsible for validating the temporary external authentication token that is passed as a query-string parameter.

WebSocket Handshake Authentication Interceptor

@Override
  public boolean beforeHandshake(ServerHttpRequest request,
                                 ServerHttpResponse response,
                                 WebSocketHandler wsHandler,
                                 Map<String, Object> attributes) throws Exception {
    UUID authToken = getAuthToken(request);
    WebSocketAuthInfo webSocketAuthInfo = getWebSocketAuthInfo(authToken);

    if (webSocketAuthInfo == null) {
      response.setStatusCode(HttpStatus.UNAUTHORIZED);
      return false;
    }

    return true;
  }

Before the handshake is established, we retrieve the temporary external authentication token from the query-string. We then check the cache to see if it is valid, and if not, we set the status to UNAUTHORIZED and return false, which cancels the handshake request. If it is valid, we return true and the handshake is made.

Cache Configuration

@Bean
  public CacheManager cacheManager() {
    CaffeineCache authCache =
      new CaffeineCache(
        "AuthCache",
        Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS).build()
    );
    SimpleCacheManager cacheManager = new SimpleCacheManager();
    cacheManager
      .setCaches(Collections.singletonList(authCache));
    return cacheManager;
  }

We have configured the cache here to expire entries 30 seconds after being written. It is assumed that the request to retrieve the temporary external authentication token will be immediately followed up by the request to open a WebSocket connection.

Summary

This blog details a pattern that can be followed to overcome the built-in obstacles of securing WebSocket communication. This could be extended by additional validation of the temporary external authentication token such as validating that the request to get the temporary token and the request to establish a WebSocket handshake come from the same IP address. Also, we could extract the HTTP endpoint from the WebSocket server by using an external cache. Have you had to secure WebSocket communications? What way did you find works best for your use case?

For more information on WebSockets and the Java Spring Framework check out their official documentation.

The source code for this blog can be found here.