Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing JWT token to SockJS

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]
like image 924
Manolis Pap Avatar asked May 03 '19 07:05

Manolis Pap


2 Answers

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

like image 93
Bhushan Uniyal Avatar answered Sep 28 '22 02:09

Bhushan Uniyal


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());
        }
    });

}
like image 27
uğur taş Avatar answered Sep 28 '22 03:09

uğur taş