4

In one of my projects I configured both rest services and websockets, both go through spring security filter that check for JWT. For websockets on the client side, application uses sockjs & stomp (on Angular2) and Spring websockets on the server side (Tomcat 8). When I open connection with Spring security enabled then I get below error two seconds after it gets opened. However when I open connection without spring security enabled connection does not get dropped. Error I get in firefox after two seconds

angular2 connect()/subscribe()/send() - all go with 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}));
    }

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

<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>
7
  • I just look at error screen and seems like you trying to connect over xdr_streaming, not over websockets. Commented Mar 30, 2017 at 12:03
  • but when security is disable for websockets "<sec:http pattern="/rest/add/**" security="none" />" everything works ok, message gets delivered to server and later passed to all subscribed users. @user1516873 you can checkout and build, it's maven and will build out of box in 5 minutes Commented Mar 30, 2017 at 12:08
  • Looks like websockets part works fine with spring security enabled, at least in my environment. Check logs pastebin.com/128L4rkz Maybe proxy problem or client browser doesn't support websockets? Commented Mar 30, 2017 at 13:42
  • @user1516873 if you see last line in the logs "Nice, I received ...(truncated)" it should be "Nice, I received ...111111111" because the message you sent was 111111111. Browser is compatible, I get exactly the same issue like you. When I check browser logs (see attached image) it says "headers is null" which tells me that when you do successful handshake it Spring security does not attach some headers that it should in the response. I checked with a few browsers and all of them work when security is disable and none works when it's enabled. Commented Mar 30, 2017 at 13:54
  • please replace in pom.xml this line "<id>npm build prod</id>" with "" this line "<id>npm build</id>" and you will see logs in your web browser Commented Mar 30, 2017 at 13:56

2 Answers 2

2

Your problem doesn't related to security. You just pass wrong arguments in Stomp connect and subscribe functions.

The connect() method also accepts two other variants 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) => {

should be

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

and

You can use the subscribe() method to subscribe to a destination. The method takes 2 mandatory arguments: destination, a String corresponding to the destination and callback, a function with one message argument and an 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) => {

should be

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

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

Sign up to request clarification or add additional context in comments.

1 Comment

unfortunately that's not it, please check the latest code on github, I made changes that you suggested but did not help. If you could please see my second post and links to JSON files with requests, once again big thanks
1

@user1516873 finally I got it working:

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

Problem was that I was using angular-cli server on port 4200 with proxy file like this:

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

but should have been like this:

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

so through all the combinations of configuration I was always checking through 4200 proxy server and very rarely through native 8080 directly. I just didn't know that angular-cli proxy does not support when spring security is applied. I will accept your answer as you helped a lot!

2 Comments

I'm glad you made it working. {transports: ["websocket"]} actually restrict SocketJs to use any other than websocket protocols, so if it cannot connects over websoket it fail immediately and not trying to failover with different protocols (like xhr_streaming).
this is basically very hopeful as well, so the recommendation is to not use {transports: ["websocket"]} if you don't have to because with it you will loose failover support to other protoculs

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.