I'm trying to establish a connection with back with Stomp (Spring Boot) using SockJS (Angular). I have registered users and anonymous ones. The anonymous ones are going to use the WebSocket connection, but while connecting I have an error.
While trying to connect:
>>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000
I receive the next answer:
<<< ERROR
message:Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.access.AccessDeniedException\c Access denied content-length:0
For Angular I'm making the connection using the following code:
private stompClient: Client = over(new SockJS(`http://localhost:8080/socket`));
constructor() {}
public connect (callback: Function): void {
this.stompClient.connect({}, frame => {
console.log(frame)
const sessionId = frame?.headers; // Here you get the Session ID
console.log(sessionId);
callback(this.stompClient)
}, (frame) => {console.error(frame)})
}
public sendMessage(endpoint: string, message: string): void {
this.stompClient.send(endpoint, {}, message)
}
public disconnet(callback: Function): void {
this.stompClient.disconnect(callback());
}
For Spring Boot Stomp connection I have the following:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket")
.setAllowedOrigins("*")
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/message", "/passport");
}
@Override
public void configureWebSocketTransport( WebSocketTransportRegistration registration )
{
registration.setMessageSizeLimit( 300000 * 50 );
registration.setSendTimeLimit( 30 * 10000 );
registration.setSendBufferSizeLimit( 3 * 512 * 1024 );
}
}
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService userDetailsService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
public void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.csrf().disable()
.authorizeRequests()
.antMatchers("/socket/**")
.permitAll()
// other endpoints
.anyRequest()
.authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
httpSecurity.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
httpSecurity.addFilterBefore(new CorsFilter(), ChannelProcessingFilter.class);
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
@AllArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtProvider jwtProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
try {
if (shouldSkipJwtFilter(request)) {
filterChain.doFilter(request, response);
return;
}
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) {
String username = jwtProvider.getUsernameFromJwt(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
} catch (ExpiredJwtException e) {
if(request.getRequestURL().toString().contains("/refresh/token")) {
allowForRefreshToken();
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
}
private boolean shouldSkipJwtFilter(HttpServletRequest request) {
return request.getRequestURI().startsWith("/socket");
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return bearerToken;
}
private void allowForRefreshToken() {
UsernamePasswordAuthenticationToken authentication;
authentication = new UsernamePasswordAuthenticationToken(null, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
@Component
public class CorsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, @NotNull FilterChain filterChain)
throws ServletException, IOException {
response.setHeader("Access-Control-Allow-Origin", "http://localhost:4200");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "authorization, content-type, xsrf-token");
response.addHeader("Access-Control-Expose-Headers", "xsrf-token");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(request, response);
}
}
}
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.simpDestMatchers("/socket/**").permitAll()
.anyMessage().authenticated();
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
Things that I tried:
- Before adding
sessionCreationPolicy(SessionCreationPolicy.STATELESS);, I had a registered user that was able to connect and use the websocket, but I don't want them to be registered as this is just for a group chat. - I also tried using a
HandshakeInterceptorfor the Stomp, but I'm not sure I implemented it correctly as it made it impossible for the users to reach the endpoint.