Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Websocket Authentication and Authorization in Spring

I've been struggling a lot to properly implement Stomp (websocket) Authentication and Authorization with Spring-Security. For posterity i'll answer my own question to provide a guide.


The Problem

Spring WebSocket documentation (for Authentication) looks unclear ATM (IMHO). And i couldn't understand how to properly handle Authentication and Authorization.


What i want

  • Authenticate users with login/password.
  • Prevent anonymous users to CONNECT though WebSocket.
  • Add authorization layer (user, admin, ...).
  • Having Principal available in controllers.

What i don't want

  • Authenticate on HTTP negotiation endpoints (since most of JavaScript libraries don't sends authentication headers along with the HTTP negotiation call).
like image 700
Anthony Raymond Avatar asked Jul 30 '17 22:07

Anthony Raymond


People also ask

How do I authenticate with WebSockets?

Authentication FlowThe 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.

How do WebSockets connect to Spring?

In order to tell Spring to forward client requests to the endpoint , we need to register the handler. Start the application- Go to http://localhost:8080 Click on start new chat it opens the WebSocket connection. Type text in the textbox and click send. On clicking end chat, the WebSocket connection will be closed.

What is authentication and authorization in Spring boot?

To know what a user can do, you first need to know who the user is. This is known as authentication. It is often done by asking for a set of credentials, such as username & password. Once verified, the client gets information about the identity and access of the user.

How do I add security to WebSockets?

Use wss:// instead of ws://. This adds a security layer over your communication. Use a server like Nginx for reverse proxying websockets and enable SSL over them.


1 Answers

As stated above the documentation looks unclear (IMHO), until Spring provide some clear documentation, here is a boilerplate to save you from spending two days trying to understand what the security chain is doing.

A really nice attempt was made by Rob-Leggett but, he was forking some Springs class and I don't feel comfortable doing so.

Things to know before you start:

  • Security chain and Security config for http and WebSocket are completely independent.
  • Spring AuthenticationProvider take not part at all in Websocket authentication.
  • The authentication won't happen on HTTP negotiation endpoint in our case, because none of the JavaScripts STOMP (websocket) libraries I know sends the necessary authentication headers along with the HTTP request.
  • Once set on CONNECT request, the user (simpUser) will be stored in the websocket session and no more authentication will be required on further messages.

Maven deps

<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> 

WebSocket configuration

The below config register a simple message broker (a simple endpoint that we will later protect).

@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {     @Override     public void configureMessageBroker(final MessageBrokerRegistry config) {         // These are endpoints the client can subscribes to.         config.enableSimpleBroker("/queue/topic");         // Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping         config.setApplicationDestinationPrefixes("/app");     }      @Override     public void registerStompEndpoints(final StompEndpointRegistry registry) {         // Handshake endpoint         registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*")     } } 

Spring security config

Since the Stomp protocol rely on a first HTTP Request, we'll need to authorize HTTP call to our stomp handshake endpoint.

@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {     @Override     protected void configure(final HttpSecurity http) throws Exception {         // This is not for websocket authorization, and this should most likely not be altered.         http                 .httpBasic().disable()                 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()                 .authorizeRequests().antMatchers("/stomp").permitAll()                 .anyRequest().denyAll();     } } 

Then we'll create a service responsible for authenticating users.
@Component public class WebSocketAuthenticatorService {     // This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class     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.");         }         // Add your own logic for retrieving user in fetchUserFromDb()         if (fetchUserFromDb(username, password) == null) {             throw new BadCredentialsException("Bad credentials for user " + username);         }          // null credentials, we do not pass the password along         return new UsernamePasswordAuthenticationToken(                 username,                 null,                 Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role         );     } } 

Note that: UsernamePasswordAuthenticationToken MUST have at least one GrantedAuthority, if you use another constructor, Spring will auto-set isAuthenticated = false.


Almost there, now we need to create an Interceptor that will set the `simpUser` header or throw `AuthenticationException` on CONNECT messages.
@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 that: preSend() MUST return a UsernamePasswordAuthenticationToken, another element in the spring security chain test this. Note that: If your UsernamePasswordAuthenticationToken was built without passing GrantedAuthority, the authentication will fail, because the constructor without granted authorities auto set authenticated = false THIS IS AN IMPORTANT DETAIL which is not documented in spring-security.


Finally create two more class to handle respectively Authorization and Authentication.
@Configuration @Order(Ordered.HIGHEST_PRECEDENCE + 99) public class WebSocketAuthenticationSecurityConfig extends  WebSocketMessageBrokerConfigurer {     @Inject     private AuthChannelInterceptorAdapter authChannelInterceptorAdapter;          @Override     public void registerStompEndpoints(final StompEndpointRegistry registry) {         // Endpoints are already registered on WebSocketConfig, no need to add more.     }      @Override     public void configureClientInboundChannel(final ChannelRegistration registration) {         registration.setInterceptors(authChannelInterceptorAdapter);     }  } 

Note that: The @Order is CRUCIAL don't forget it, it allows our interceptor to be registered first in the security chain.

@Configuration public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {     @Override     protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {         // You can customize your authorization mapping here.         messages.anyMessage().authenticated();     }      // TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint.     @Override     protected boolean sameOriginDisabled() {         return true;     } } 
like image 93
Anthony Raymond Avatar answered Oct 14 '22 12:10

Anthony Raymond