javaangularspring-bootspring-websocketstompjs

CORS issue when working with spring sercurity and websocket with stompjs and sockjs-client


It is an error when I want to add websocket into my web.

This is my WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
    }

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

This is my Spring SercurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final UserDetailsService userDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean());
        customAuthenticationFilter.setFilterProcessesUrl("/api/login");
        
        http.csrf().disable().cors().configurationSource(request -> {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
            configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
            configuration.addAllowedHeader("*");
            return configuration;
        }).and()
        // .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        // .authorizeHttpRequests().antMatchers("/api/login/**", "/api/token/refresh/**", "/api/user/save/**","/socket/**", "/socket/**").permitAll().and()
        //         .authorizeHttpRequests().antMatchers(HttpMethod.GET, "/api/users/**").hasAnyAuthority("ROLE_USER").and()
        //         .authorizeHttpRequests().antMatchers(HttpMethod.GET, "/api/posts/**").hasAnyAuthority("ROLE_USER").and()
        // .authorizeHttpRequests().anyRequest().authenticated().and()
        .authorizeRequests().anyRequest().permitAll().and()
        .addFilter(customAuthenticationFilter)
        .addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

This is my CustomAuthorizationFilter

public class CustomAuthorizationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/token/refresh")) {
            filterChain.doFilter(request, response);
        } else {
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                try {
                    String token = authorizationHeader.substring("Bearer ".length());
                    Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
                    JWTVerifier verifier = JWT.require(algorithm).build();
                    DecodedJWT decodedJWT = verifier.verify(token);

                    String username = decodedJWT.getSubject();
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    
                    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    Arrays.stream(roles).forEach(role -> {
                        authorities.add(new SimpleGrantedAuthority(role));
                    });

                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                            username, null, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                    filterChain.doFilter(request, response);
                } catch (Exception exception) {
                    log.error("Error logging in: {}", exception.getMessage());
                    response.setHeader("error", exception.getMessage());
                    response.setStatus(FORBIDDEN.value());

                    Map<String, String> error = new HashMap<>();
                    error.put("error_message", exception.getMessage());
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);

                    new ObjectMapper().writeValue(response.getOutputStream(), error);
                }
            } else {
                filterChain.doFilter(request, response);
            }

        }
    }
}

And interceptor token and service I use to connect socket socket in angular

@Injectable()
export class TokenInterceptor implements HttpInterceptor {

    constructor(private auth: AuthService, private router: Router) { }

    intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> | any {
        const myToken = this.auth.getToken();

        if (myToken) {
            request = request.clone({
                setHeaders: {
                    Authorization: `Bearer ${myToken}`
                },
            });
        }

        return next.handle(request).pipe(
            catchError((err: any) => {
                if (err instanceof HttpErrorResponse) {
                    if (err.status === 401) {
                        return this.handleUnAuthorizedError(request, next)
                    }
                }
                return throwError(() => new Error("Some other error occured"))
            })
        );
    }

    handleUnAuthorizedError(req: HttpRequest<any>, next: HttpHandler) {
        let tokenApiModel = new TokenApiModel();
        tokenApiModel.access_token = this.auth.getToken()!;
        tokenApiModel.refresh_token = this.auth.getRefreshToken()!;
        return this.auth.renewToken(tokenApiModel)
            .pipe(
                switchMap((data: TokenApiModel) => {
                    this.auth.storeRefreshToken(data.refresh_token)
                    this.auth.storeToken(data.access_token)
                    req = req.clone({
                        setHeaders: {
                            Authorization: `Bearer ${data.access_token}`
                        },
                        withCredentials: false
                    });
                    return next.handle(req)
                }),
                catchError((err) => {
                    return throwError(() => {
                        alert("Token is expired, login again")
                        this.router.navigate(["login"])
                    })
                })
            )
    }
}
@Injectable({
  providedIn: 'root'
})
export class SocketService {
  public stompClient: any;
  public msg: Array<any> = [];

  constructor() {
    const serverUrl = 'http://localhost:8080/socket';
    const ws = new SockJS(serverUrl);
    this.stompClient = Stomp.over(ws);
  }

  initializeWebSocketConnection() {
    const that = this;
    console.log(this.stompClient); // It work until here 

    this.stompClient.connect({}, function (frame: any) {
      that.stompClient.subscribe('/message', (message: any) => {
        console.log(message);
        if (message.body) {

          that.msg.push(message.body);
        }
      });
      that.stompClient.subscribe('/messages', (message: any) => {
        console.log(message);
        if (message.body) {

          that.msg.push(message.body);
        }
      });
    });
  }

  sendMessage(message: any) {
    this.stompClient.send('/app/send/message', {}, message);
  }
}

This is logger error error

I think that it is maybe a conflict of spring security and websocket config


Solution

  • As the error said so I think you have to set configuration.setAllowCredentials(true);