Spring

 How to authenticate Web Socket endpoints with JWT and Spring Boot

Anand Sehgal's profile image
Anand Sehgal posted Mar 04, 2020 07:01 AM

I have created a chat application using spring web sockets in server and Stomp with Sock JS in client. I am facing issues in authentication with JWT. I am able to authenticate connection with server.

When I subscribe to a destination and hit the endpoint as below -

stompClient.send('/app/chat/username/'+username, header,{});

 

I am getting the error following error -

<<< ERROR   message:Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.access.AccessDeniedException\c Access is denied   content-length:0

 

 

I have implemented security config class as below-

@Configuration   public class WebSocketSecurityConfig   extends AbstractSecurityWebSocketMessageBrokerConfigurer {     @Override   protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {   messages   .nullDestMatcher().authenticated()   .simpSubscribeDestMatchers("/user/queue/errors").permitAll()   .simpDestMatchers("/app/**").hasRole("USER")   .simpSubscribeDestMatchers("/ws/**","/user/**", "/topic/**","/queue/**").hasRole("USER")   .simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll()   .anyMessage().denyAll();   }     }   }  

 

 

In have implemented authentication config as below-

 

@Configuration   @EnableWebSocketMessageBroker   @Order(Ordered.HIGHEST_PRECEDENCE + 99)   public class WebSocketAuthenticationConfig implements WebSocketMessageBrokerConfigurer {     private static final Logger logger = LoggerFactory.getLogger(WebSocketAuthenticationConfig.class);   @Autowired   private JwtTokenUtil jwtTokenUtil;     @Autowired   private UserDetailsService userDetailsService;     @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);   logger.info("************ STOMP COMMAND *****"+accessor.getCommand());   logger.info("STOMP access destination "+accessor.getDestination());     if (StompCommand.CONNECT.equals(accessor.getCommand())) {   if (accessor.getNativeHeader("Authorization") != null) {   List<String> authorization = accessor.getNativeHeader("Authorization");   logger.info("Authorization: {}", authorization);   String accessToken = authorization.get(0).split(" ")[1];   logger.info("Access Token ---- " + accessToken);   String username = jwtTokenUtil.getUsernameFromToken(accessToken);   JwtUser userDetails = (JwtUser) userDetailsService.loadUserByUsername(username);   if (username != null) {   logger.debug("security context was null, so authorizating user");   if (jwtTokenUtil.validateToken(accessToken, userDetails)) {   UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(   userDetails, null, userDetails.getAuthorities());   logger.info("authorizated user '{}', setting security context", username);   if(SecurityContextHolder.getContext().getAuthentication() == null)   SecurityContextHolder.getContext().setAuthentication(authentication);   accessor.setUser(authentication);   }   if(StompCommand.SEND.equals(accessor.getCommand())) {   boolean isSent = channel.send(message);   logger.info("Message sent "+isSent);   if(isSent)   return message;   }   }   }   }   else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {   Authentication authentication = SecurityContextHolder.getContext().getAuthentication();   if (Objects.nonNull(authentication))   logger.info("Disconnected Auth : " + authentication.getName());   else   logger.info("Disconnected Sess : " + accessor.getSessionId());   }   return message;   }   });   }   }  

 

Daniel Mikusa's profile image
Daniel Mikusa

It's a little hard to tell what is happening here. Something isn't right with the authentication/authorization based on the error, but it's not clear where it's going wrong. I'm also not sure I understand what you're trying to do with `preSend(..)`.

 

I would suggest taking a look at this sample applicationhttps://github.com/spring-projects/spring-session/tree/master/spring-session-samples/spring-session-sample-boot-websocket

 

It shows how to set up WebSockets & secure messages. You can then adjust the Spring Security configuration to enable OAuth2 support.

 

I haven't done it, but based on the docs...

 

>WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.

 

https://docs.spring.io/spring-security/site/docs/5.2.2.RELEASE/reference/htmlsingle/#websocket-authentication

 

I don't think you need to do anything special to enable OAuth2 & JWT support. If you use Spring Security's standard configuration to set up a resource server, https://docs.spring.io/spring-security/site/docs/5.2.2.RELEASE/reference/htmlsingle/#oauth2resourceserver, based on the doc comment above the app should just have access to the authentication information for securing your message endpoints.

 

Having said all that, I don't know anything about the app you're trying to build or requirements you have, so your mileage may vary. Hope this points you in the right direction.