11

I need to send the token when the handshake happens with SockJS. I've tried many suggested implementations but the same exception is called

java.lang.IllegalArgumentException: JWT String argument cannot be null or empty.

In the back-end WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker
@CrossOrigin
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/websocket").withSockJS();
    }
}

The function that tries to make the connection with the socket. Plain javascript.

function connect() {
    var socket = new SockJS('http://localhost:8889/websocket',
             null,
            {
                transports: ['xhr-streaming'], 
                headers: {'Authorization': 'Bearer eyJhbGciOiJIUzUxMiJ9...' }
            });
    stompClient = Stomp.over(socket);
    stompClient.connect({},function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/socket/event', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        });
    });
}

The issue is on the handshake, those headers doesn't seem to pass the token correctly. I've tried many variations on the handshake but I can't find the correct on in my case.

I got the implementation idea from here, before I tried to use the headers after the handshake but I figure out that it needs the token immediately.

https://github.com/sockjs/sockjs-client/issues/196#issuecomment-61469141

Edit: Adding WebSecurityConfig

@Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
        .cors()
        .configurationSource(request -> new CorsConfiguration().applyPermitDefaultValues())
        .and()
        .csrf()
        .disable()
        .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()

        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
        .authorizeRequests()
        .antMatchers("/login/**").permitAll()
        .antMatchers("/websocket/**").permitAll()
        .anyRequest().authenticated();
        // Custom JWT based security filter
        JwtAuthorizationTokenFilter authenticationTokenFilter = new JwtAuthorizationTokenFilter(userDetailsService(), jwtTokenUtil, tokenHeader);
        httpSecurity
        .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }//end configure(HttpSecurity)

createAuthenticationToken

@ApiOperation(value = "Login with the user credentials",
            response = JwtAuthenticationResponse.class)
    @ApiResponses(value = {
            @ApiResponse(code = 401, message = "Unauthorized"),
            @ApiResponse(code = 404, message = "Not Found",response = ExceptionResponse.class),
            @ApiResponse(code = 400, message = "Bad Request",response = ExceptionResponse.class),
            @ApiResponse(code = 200 , message = "OK", response = JwtAuthenticationResponse.class)
    })
    @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(
            @ApiParam(value = "User's email and password", required = true)
            @RequestBody JwtAuthenticationRequest authenticationRequest) 
            throws AuthenticationException {
        ResponseEntity<?> response;
        //authenticate the user
        final User user = userService.getByEmail(authenticationRequest.getEmail());
        try {
            authenticate(user.getUsername(), authenticationRequest.getPassword(),user.getId(),user.getAuthority().getName());
            // Reload password post-security so we can generate the token
            final UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
            final String token = jwtTokenUtil.generateToken(userDetails);
            // Return the token
            response  = ResponseEntity.ok(new JwtAuthenticationResponse(token,user.getUsername(),user.getFirstName(),user.getLastName(),
                    user.getEmail(),user.getId(),user.getAuthority().getName(),jwtTokenUtil.getExpirationTime(token)));
        }catch(NullPointerException e) {
            response = new ResponseEntity<>(new ExceptionResponse(404,"User Not Found","Authentication Failure"),HttpStatus.NOT_FOUND);
        }catch(AuthenticationException e) {
            response = new ResponseEntity<>(new ExceptionResponse(400,"Invalid E-mail or Password","Authentication Failure"),HttpStatus.BAD_REQUEST);
        }//end try
                return response;
    }//end createAuthenticationToken(JwtAuthenticationRequest)

Stack trace (the same exception has been caught four times when the handshake and connection happens from the websocket with the back-end). I added it on pastebin because it would ruin the post.

Exception

2019-05-16 11:36:17.936  WARN 11584 --- [nio-8889-exec-9] a.d.s.JwtAuthorizationTokenFilter        : couldn't find bearer string, will ignore the header
2019-05-16 11:36:17.937 ERROR 11584 --- [nio-8889-exec-9] a.d.s.JwtAuthorizationTokenFilter        : an error occured during getting username from token

java.lang.IllegalArgumentException: JWT String argument cannot be null or empty.
    at io.jsonwebtoken.lang.Assert.hasText(Assert.java:135) ~[jjwt-0.9.0.jar:0.9.0]
    at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:479) ~[jjwt-0.9.0.jar:0.9.0]
    at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJws(DefaultJwtParser.java:541) ~[jjwt-0.9.0.jar:0.9.0]
    at package.security.JwtTokenUtil.getAllClaimsFromToken(JwtTokenUtil.java:59) ~[classes/:na]
    at package.security.JwtTokenUtil.getClaimFromToken(JwtTokenUtil.java:52) ~[classes/:na]
    at package.security.JwtTokenUtil.getUsernameFromToken(JwtTokenUtil.java:34) ~[classes/:na]
    at package.security.JwtAuthorizationTokenFilter.extractUsername(JwtAuthorizationTokenFilter.java:79) [classes/:na]
    at package.security.JwtAuthorizationTokenFilter.doFilterInternal(JwtAuthorizationTokenFilter.java:44) [classes/:na]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:116) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ...
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:105) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:56) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:331) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:177) [spring-security-web-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:346) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:262) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) [spring-web-4.3.11.RELEASE.jar:4.3.11.RELEASE]
    ...
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:799) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1457) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [na:1.8.0_201]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [na:1.8.0_201]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.20.jar:8.5.20]
    at java.lang.Thread.run(Unknown Source) [na:1.8.0_201]
Manuel Pap
  • 1,309
  • 7
  • 23
  • 52
  • 1
    In which line you get `IllegalArgumentException` / Do you have all stack trace of exception ? – smilyface May 15 '19 at 14:12
  • @smilyface I added the configure method on WebSecurityConfig. It happens on every call on `addFilterBefore` at the bottom of the method. It makes sense because the same thing is happening when I make HTTP calls and there is no token. But with SockJS I am trying to pass the token and still it doesn't arrive on the back-end. The websocket communication is happening without any problems. – Manuel Pap May 16 '19 at 06:49
  • hmm that's ok. But you didn't answered my question (at least 10-15 lines of stack trace) – smilyface May 16 '19 at 08:20
  • 1
    @smilyface I added the complete stack trace on pastebin. The same exception has been cought four times. Possibly the headers are triggering the exception. – Manuel Pap May 16 '19 at 08:39
  • Also provide the method to create the token (if possible an example token too ). I guess it could be something like .. `Jwts.builder().something..` to create the token. Please include that too in the question – smilyface May 16 '19 at 08:49
  • As you mentioned about handshake.. can you change the allowing path method to `.antMatchers(HttpMethod.OPTIONS, "**").permitAll()` - just to confirm that is not the issue. – smilyface May 16 '19 at 08:54
  • @smilyface The token is being created on login, I don't see how to creation of the token is relevant. Anyway, I added the method `createAuthenticationToken` and an example token can be seen on javascript client `Bearer ey.....` – Manuel Pap May 16 '19 at 08:55
  • @smilyface I tried it, same exception. Also, I added permitAll for the websocket endpoint just to be able to use it without JWT. – Manuel Pap May 16 '19 at 09:20

2 Answers2

4

server-side configuration to register a custom authentication interceptor. Note that an interceptor needs only to authenticate and set the user header on the CONNECT Message. Spring notes and saves the authenticated user and associate it with subsequent STOMP messages on the same session. The following example shows how register a custom authentication interceptor:

  @Configuration
    @EnableWebSocketMessageBroker
    public class MyConfig implements WebSocketMessageBrokerConfigurer {

        @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);
                    if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                        Authentication user = ... ; // access authentication header(s)
                        accessor.setUser(user);
                    }
                    return message;
                }
            });
        }
    }

Also, note that, when you use Spring Security’s authorization for messages, at present, you need to ensure that the authentication ChannelInterceptor config is ordered ahead of Spring Security’s. This is best done by declaring the custom interceptor in its own implementation of WebSocketMessageBrokerConfigurer that is marked with @Order(Ordered.HIGHEST_PRECEDENCE + 99).

Another way : Likewise, the SockJS JavaScript client does not provide a way to send HTTP headers with SockJS transport requests. As you can see sockjs-client issue 196. Instead, it does allow sending query parameters that you can use to send a token and then with Spring you can setup some filter which will identify a session using provided token. , but that has its own drawbacks (for example, the token may be inadvertently logged with the URL in server logs).

Ref

Bhushan Uniyal
  • 5,575
  • 2
  • 22
  • 45
  • Ι saw the query solution and didn't like it, so I am gonna pass on that. I get that the authorization header should be on the communication, but Spring needs a token on handshake. I guess now I have to find a way not get an exception when handshake happens. Not satisfied with the security aspect on sockets. – Manuel Pap May 17 '19 at 07:19
  • Anyway, since the bounty will be lost I'll give it to you. But I am still leaving it open for the future, maybe a solution or a working example will appear. – Manuel Pap May 22 '19 at 14:03
2

Websocket's doesn't follows the same pattern in headers with HTTP. That's why, Even if you send token in header, It could not found. I had the same issue before and I changed websocket security structure.

My sample code is this:

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
    registration.setInterceptors(new ChannelInterceptorAdapter() {

        @Override
        public Message<?> preSend(Message<?> message, MessageChannel channel) {
            StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
            MessageHeaders headers = message.getHeaders();
            SimpMessageType type = (SimpMessageType) headers.get("simpMessageType");
            List<String> tokenList = accessor.getNativeHeader("Authorization");
            String token = null;
            if(tokenList == null || tokenList.size() < 1) {
                return message;
            } else {
                token = tokenList.get(0);
                if(token == null) {
                    return message;
                }
            }

            // validate and convert to a Principal based on your own requirements e.g.
            // authenticationManager.authenticate(JwtAuthentication(token))
            try{
                JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(new RawAccessJwtToken(tokenExtractor.extract(token)));
                Authentication yourAuth = jwtAuthenticationProvider.authenticate(jwtAuthenticationToken);
                accessor.setUser(yourAuth);
            } catch (Exception e) {
                throw new IllegalArgumentException(e.getMessage());
            }




            // not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
            accessor.setLeaveMutable(true);
            return MessageBuilder.createMessage(message.getPayload(), accessor.getMessageHeaders());
        }
    });

}
uğur taş
  • 355
  • 1
  • 6
  • 19
  • I implemented the method based on your implementation, but sadly the method doesn't seem to be called. Maybe I am missing something. Edit: It seems that it's been called after all, I will figure it out more how it works and will get back. – Manuel Pap May 16 '19 at 11:57
  • That method is for the connection, we need to pass the header on handshake. – Manuel Pap May 16 '19 at 12:38
  • 1
    With this code, you will check it every time a message sent. – uğur taş May 17 '19 at 08:42
  • I know and appreciate but I am looking something for handshake. If I manage to find a workaround for the handshake, I will get back to you. – Manuel Pap May 17 '19 at 08:48