Tech Info

  1. Main page
  2. Backend
  3. Main content

WebSocket Authentication and Authorization in Spring

2023-10-06 523hotness 0likes 0comments

I. Things to Know

  • The security chain and security configuration of HTTP and WebSocket are completely independent.
  • SpringAuthenticationProvider is not involved in WebSocket authentication at all.
  • In the examples given, authentication will not occur on the HTTP negotiation endpoint, because the JavaScript STOMP (websocket) libraries do not send the necessary authentication headers along with the HTTP request.
  • Once set on the CONNECT request, the user (simpUser) will be stored in the websocket session, and subsequent messages will no longer need authentication.

II. Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>  
    <artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-messaging</artifactId> 
</dependency>

III. WebSocket Configuration

3.1, Simple Message Broker

@Configuration
@EnableWebSocketMessageBroker  
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(final MessageBrokerRegistry config) {
        config.enableSimpleBroker("/queue/topic");
        config.setApplicationDestinationPrefixes("/app"); 
    }

    @Override   
    public void registerStompEndpoints(final StompEndpointRegistry registry) {
        registry.addEndpoint("stomp");
        setAllowedOrigins("*") 
    }
}

3.2, Spring Security Configuration

Since the Stomp protocol relies on the first HTTP request, authorization for the stomp handshake endpoint HTTP call is required.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(final HttpSecurity http) throws Exception
        http.httpBasic().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests().antMatchers("/stomp").permitAll()
                .anyRequest().denyAll();
    }
}

Then create a service responsible for verifying user identity.

@Component
public class WebSocketAuthenticatorService {
    public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String  username, final String password) throws AuthenticationException {
        if (username == null || username.trim().isEmpty()) {
            throw new AuthenticationCredentialsNotFoundException("Username was null or empty.");
        }
        if (password == null || password.trim().isEmpty()) {
            throw new AuthenticationCredentialsNotFoundException("Password was null or empty.");
        }

        if (fetchUserFromDb(username, password) == null) {
            throw new BadCredentialsException("Bad credentials for user " + username);
        }


        return new UsernamePasswordAuthenticationToken(
                username,
                null,
                Collections.singleton((GrantedAuthority) () -> "USER") // Must give at least one role
        );
    }
}

Next you need to create an interceptor that will set the "simpUser" header or throw an "AuthenticationException" on the CONNECT message.

@Component
public class AuthChannelInterceptorAdapter extends ChannelInterceptor {
    private static final String USERNAME_HEADER = "login";
    private static final String PASSWORD_HEADER = "passcode";
    private final WebSocketAuthenticatorService webSocketAuthenticatorService;

    @Inject
    public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) {
        this.webSocketAuthenticatorService = webSocketAuthenticatorService;
    }

    @Override
    public Message<?> preSend(final Message<?> message, final MessageChannel channel) throws AuthenticationException {
        final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        if (StompCommand.CONNECT == accessor.getCommand()) {
            final String username = accessor.getFirstNativeHeader(USERNAME_HEADER);
            final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER);

            final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password);

            accessor.setUser(user);
        }
        return message;
    }
}

Note: preSend() must return UsernamePasswordAuthenticationToken, this will be tested in the Spring security chain. If the UsernamePasswordAuthenticationToken construction does not pass GrantedAuthority, authentication will fail, because the constructor without granted authorities automatically sets authenticated = false. This is an important detail not documented in spring-security.

Finally, create two more classes to handle authorization and authentication separately.

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE + 99) 
public class WebSocketAuthenticationSecurityConfig extends  WebSocketMessageBrokerConfigurer {
    @Inject
    private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;

    @Override
    public void registerStompEndpoints(final StompEndpointRegistry registry) {
        // Nothing to do here
    }

    @Override
    public void configureClientInboundChannel(final ChannelRegistration registration) {
        registration.setInterceptors(authChannelInterceptorAdapter);
    }

}

Note: This @Order is critical, it allows our interceptor to register first in the security chain.

@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
    @Override
    protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
        // Add your own mappings
        messages.anyMessage().authenticated();
    }

    // Modify as needed 
    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}

After that, write the client to connect, we can specify the client to send messages like this.

  @MessageMapping("/greeting")
    public void greetingReturn(@Payload Object ojd){
         simpMessagingTemplate.convertAndSendToUser(username,"/topic/greeting",ojd);
    }

Related

This article is licensed with Creative Commons Attribution 4.0 International License
Tag: Security Spring Boot WebSocket
Last updated:2023-10-06

jimmychen

This person is a lazy dog and has left nothing

Like
< Last article
Next article >

Comments

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
Cancel

Archives
  • October 2023
  • September 2023
Categories
  • Algorithm
  • Android
  • Backend
  • Embedded
  • Security
Ads

COPYRIGHT © 2023 Tech Info. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang