Skip to main content

Spring WebSocket authentication with JWT Spring Security

1. Overview



In this tutorial, we will talk about how to authenticate your Spring websocket server using JWT Spring Secutiry. Suppose that you already have a stateless microservice with an authorisation server up and running. Now you would want that all of your websocket service instances have to connect to the authorisation service to validate user's access token before allowing them to establish a websocket connection to your server. The official Spring document [1] has mentioned about this. However, they don't clarify how can we obtain the Authentication object. One can write a custom token validation mechanism and validate the user header token manually. But in our opinion, it is best to avoid doing this, as it may introduce some security vulnerabilities.

In this tutorial, we will walk you though step-by-step of how to validate user's access token by reusing the JwtAuthentication provided by Spring Security framework.

2. Gradle Dependencies


Since this is a Gradle-based project, we first add the required dependencies to the build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-reactor-netty'
implementation 'org.springframework.boot:spring-boot-starter-websocket'

3. Enable WebSocket in Spring


First, we enable the WebSocket capabilities by adding a configuration to our application and annotate this class with @EnableWebSocketMessageBroker, as follow:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket");
        registry.addEndpoint("/websocket").withSockJS();
    }
}
Now, what we want to do is to secure the STOMP endpoint /websocket to ensure that any one who wants to establish a websocket connection to our service has to be authenticated. 

4. Configure WebSocket authentication


Since our services are stateless, therefore we can't rely on cookie session to validate user. Instead, we can validate the access token in the header of the CONNECT handshake request. 
In order to do so, we can define an interceptor to intercept this request and validate it as follow:
@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Autowired
    private AuthenticationProvider authenticationProvider;

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message preSend(Message message, MessageChannel channel) {
                StompHeaderAccessor accessor =
                        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    String token = accessor.getFirstNativeHeader("Authorization");
                    if (token == null || !token.startsWith("Bearer ")) {
                        throw new AccessDeniedException("Unauthorised");
                    }
                    token = token.substring(7);
                    BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
                    Authentication user = authenticationProvider.authenticate(authenticationRequest);
                    if (!user.isAuthenticated()) {
                        throw new AccessDeniedException("Unauthorised");
                    }
                    accessor.setUser(user);
                }
                return message;
            }
        });
    }
}
Here we have configured our service to validate user's access token before establising the websocket connection. We use a bean's object of class AuthenticationProvider to validate the Authorization header of the connecting user.  If and only if the user is validated, then we allow the user to establish a websocket connection, otherwise, we reject by throwing an AccessDeniedException

5. Configure Spring Security


Here, we define the authentication logic for our websocket service. The first thing we need to do is configuring our security context to reject all but the websocket endpoints.
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeRequests((authorize) -> authorize
                        .antMatchers("/websocket").permitAll()
                        .antMatchers("/websocket/**").permitAll()
                        .anyRequest().denyAll());
        return http.build();
    }
}
Here we set our webserver as stateless, as we don't want Spring Security to create any session. We allow the requests to go through /websocket and /websocket/** since these are the endpoints that will be used by Spring Websocket and will be authenticated by the interceptor that we have defined in the previous section. Any other requests will be denied.

Next, we will define the AuthenticationProvider that we have mentioned earlier. From the same WebSercurityConfig class, we define the configurations as follow:
@EnableWebSecurity
public class WebSecurityConfig {

    private final JWSAlgorithm jwsAlgorithm = JWSAlgorithm.RS256;

    private final JWEAlgorithm jweAlgorithm = JWEAlgorithm.RSA_OAEP_256;

    private final EncryptionMethod encryptionMethod = EncryptionMethod.A256GCM;

    @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
    URL jwkSetUri;

    @Value("${auth.jwe-key-value}")
    RSAPrivateKey key;


    @Bean
    public AuthenticationProvider getJwsAuthenticationProvider(JwtDecoder decoder) {
        JwtAuthenticationProvider provider = new JwtAuthenticationProvider(decoder);
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        provider.setJwtAuthenticationConverter(converter);
        return provider;
    }

    @Bean
    JwtDecoder jwtDecoder() {
        return new NimbusJwtDecoder(jwtProcessor());
    }

    private JWTProcessor jwtProcessor() {
        JWKSource jwsJwkSource = new RemoteJWKSet<>(this.jwkSetUri);
        JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(this.jwsAlgorithm,
                jwsJwkSource);

        JWKSource jweJwkSource = new ImmutableJWKSet<>(new JWKSet(rsaKey()));
        JWEKeySelector jweKeySelector = new JWEDecryptionKeySelector<>(this.jweAlgorithm,
                this.encryptionMethod, jweJwkSource);

        ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
        jwtProcessor.setJWSKeySelector(jwsKeySelector);
        jwtProcessor.setJWEKeySelector(jweKeySelector);

        return jwtProcessor;
    }

    private RSAKey rsaKey() {
        RSAPrivateCrtKey crtKey = (RSAPrivateCrtKey) this.key;
        Base64URL n = Base64URL.encode(crtKey.getModulus());
        Base64URL e = Base64URL.encode(crtKey.getPublicExponent());
        return new RSAKey.Builder(n, e).privateKey(this.key).keyUse(KeyUse.ENCRYPTION).build();
    }
}

It can be seen that we have reused the Spring's built-in JwtAuthenticationProvider with JWTProcessor as the JWTDecoder to decode the JWT access token. The JWTProcessor decode the JWT access token by checking with the authorisation server via jwkSetUri using it own RSAPrivateKey.

With this setup, your websocket service is now able to connect to your authorisation server to verify user's access token. 

A simple implementation of this tutorial can be found here: https://github.com/dispatcher-servlet/websocket-jwt-oauth2

For testing this JWT websocket authentication, please refer to https://dispatcherservlet.blogspot.com/2022/10/spring-websocket-integration-test-with.html

[1] https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-authentication-token-based

Comments

Popular posts from this blog

Spring Websocket integration test with JWT Authentication

1. Overview In our  previous tutorial  we have introduced an approach to authenticate a user when connecting to our websocket server using JWT authentication.  In this article, we will see how can we perform integration tests to check the behaviours of our security layers in different scenarios, as well as ensuring the correctness of our implementation for future updates. 2. Gradle dependencies Since this is a Gradle-based project, we add the required testing dependencies to the build.gradle: testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0' testImplementation 'org.awaitility:awaitility-kotlin:4.2.0' testImplementation 'io.projectreactor:reactor-test' 3. Defining a Controller endpoint for testing In this example, we will write some integration tests to validate the requests to access a controller endpoint, as defined below: @Controller public class WebsocketCon...