Spring-security block websocket (sockjs)

In one of my projects, I configured rest services and websockets, both went through a spring security filter that validates the JWT. For client side web sites, the application uses sockjs and stomp (on Angular2) and spring server side web sites (Tomcat 8). When I open a connection with spring security then I get below error two seconds after opening it. However, when I open a non-spring connection, the secure connection is not dropped. The error I am getting through firefox after two seconds

angular2 connect () / subscribe () / send () - everything comes with a JWT token

public connect() : void {
        let sockjs = new SockJS('/rest/add?jwt=' + this.authService.getToken());
        let headers : any = this.authService.getAuthHeader();
        this.stompClient = Stomp.over(sockjs);
        this.stompClient.connect(this.token, (frame) => {
            this.log.d("frame", "My Frame: " + frame);
            this.log.d("connected()", "connected to /add");
            this.stompClient.subscribe('/topic/addMessage', this.authService.getAuthHeader(), (stompResponse) => {
                // this.stompSubject.next(JSON.parse(stompResponse.body));
                this.log.d("result of WS call: ", JSON.parse(stompResponse.body).message);
            }, (error) => {
                this.log.d(error);
            });
        });
    }

    public send(payload: string) {
        this.stompClient.send("/app/add", this.token, JSON.stringify({'message': payload}));
    }
      

Run codeHide result


JwtAuthenticationFilter.java

public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    public JwtAuthenticationFilter() {
        super("/rest/**");
    }

    @Override
    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        return true;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String token = null;

        String param = request.getParameter("jwt");
        if(param == null) {
            String header = request.getHeader("Authorization");
            if (header == null || !header.startsWith("Bearer ")) {
                throw new JwtAuthenticationException("No JWT token found in request headers");
            }
            token = header.substring(7);
        } else {
            token = param;
        }
        JwtAuthenticationToken authRequest = new JwtAuthenticationToken(token);

        return getAuthenticationManager().authenticate(authRequest);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);

        // As this authentication is in HTTP header, after success we need to continue the request normally
        // and return the response as if the resource was not secured at all
        chain.doFilter(request, response);
    }
}

      

JwtAuthenticationProvider.java

@Service
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    @Autowired
    private SecurityService securityService;

    @Override
    public boolean supports(Class<?> authentication) {
        return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
    }

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
    }

    @Override
    @Transactional(readOnly=true)
    protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        JwtAuthenticationToken jwtAuthenticationToken = (JwtAuthenticationToken) authentication;
        String token = jwtAuthenticationToken.getToken();

        User user = securityService.parseToken(token);

        if (user == null) {
            throw new JwtAuthenticationException("JWT token is not valid");
        }

        return new AuthenticatedUser(user);
    }
}

      

JwtAuthenticationSuccessHandler.java

@Service
public class JwtAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // We do not need to do anything extra on REST authentication success, because there is no page to redirect to
    }

}

      

RestAuthenticationEntryPoint.java

@Service
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        // This is invoked when user tries to access a secured REST resource without supplying any credentials
        // We should just send a 401 Unauthorized response because there is no 'login page' to redirect to
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

      

Weboscket config:

<websocket:message-broker
    application-destination-prefix="/app">
    <websocket:stomp-endpoint path="/add">
        <websocket:sockjs />
    </websocket:stomp-endpoint>
    <websocket:simple-broker prefix="/topic, /queue" />
</websocket:message-broker>

      

and my spring security

<context:component-scan base-package="com.myapp.ws.security"/>

<sec:global-method-security pre-post-annotations="enabled" />

<!-- everyone can try to login -->
<sec:http pattern="/rest/login/" security="none" />
<!--<sec:http pattern="/rest/add/**" security="none" />-->

<!-- only users with valid JWT can access protected resources -->
<sec:http pattern="/rest/**" entry-point-ref="restAuthenticationEntryPoint" create-session="stateless">
    <!-- JWT is used to disabled-->
    <sec:csrf disabled="true" />
    <!-- don't redirect to UI login form -->
    <sec:custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter" />
</sec:http>

<bean id="jwtAuthenticationFilter" class="com.myapp.ws.security.JwtAuthenticationFilter">
    <property name="authenticationManager" ref="authenticationManager" />
    <property name="authenticationSuccessHandler" ref="jwtAuthenticationSuccessHandler" />
</bean>

<sec:authentication-manager alias="authenticationManager">
    <sec:authentication-provider ref="jwtAuthenticationProvider" />
</sec:authentication-manager>

      

+3


source to share


2 answers


@ user1516873 finally i got it:

  • passing correct parameters to STOMP fixed one problem
  • adding {transports: ["websocket"]} was not necessary (it works without it)

The problem was I was using an angular-cli server on port 4200 with a proxy file like this:

{"/ rest": {"target": " http: // localhost: 8080 ", "secure": false}}



but it should be like this:

{"/ rest": {"target": " http: // localhost: 8080 ", "secure": false, "ws": true, "logLevel": "debug"}}

so through all configuration combinations I have always checked the 4200 proxy and very rarely through the native 8080 directly. I just didn't know that angular-cli proxy doesn't support when spring protection is applied. I will accept my answer as you helped a lot!

+1


source


Your problem is not security related. You are just passing the wrong arguments to the Stomp connect and subscription function.

The connect () method also accepts two other options if you need to pass additional headers:

client.connect (headers, connectCallback);
  client.connect (headers, connectCallback, errorCallback);

where header is a map and connectCallback and errorCallback are functions.

this.stompClient.connect(this.token, (frame) => {

      

it should be

this.stompClient.connect({}, (frame) => {

      

and



You can use the subscribe () method to subscribe to a destination. the method takes 2 required arguments: destination, a String corresponding to the destination and callback, a function with one message argument and optional argument headers, a JavaScript object for additional headers.

var subscription = client.subscribe ("/ queue / test", callback);

this.stompClient.subscribe('/topic/addMessage', this.authService.getAuthHeader(), (stompResponse) => {

      

it should be

this.stompClient.subscribe('/topic/addMessage', (stompResponse) => {

      

Documentation http://jmesnil.net/stomp-websocket/doc/

+2


source







All Articles