Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Websocket Security throwing AccessDeniedException

Let me preface this by saying that access should be denied for my scenario. Built in Spring Boot with Spring Security 4. I'm allowing anyone to connect to the websocket and subscribe to a topic, but I'm securing the ability to send messages to the topic, with the following web socket security config:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpSubscribeDestMatchers("/main-page-feed/thought-queue/**").permitAll()
                .simpDestMatchers("/thought-bubble/push-to-queue/**").authenticated();


    }

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }

}

So when you try to send a message to /thought-bubble/push-to-queue while unauthenticated, it denies you (which is correct, I want to emphasize this because the only other questions I can find on this are when the exception is incorrectly thrown) and throws an AccessDeniedException. What I'm not understanding is when, contrary to websocket security, Spring HTTP security locks something down and refuses access, it doesn't throw an exception, it just sends an HTTP status. I've tried using @ExceptionHandler, AccessDenied handlers, but nothing I've tried has been able to catch and handle this exception. Below is the stack trace and other relevant files, any ideas are appreciated because I'm pretty stuck. I've tried stepping through the source code in debug but I'm not really seeing what the issue is.

Stack Trace:

org.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:127) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:104) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:298) ~[spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.handleMessage(SubProtocolWebSocketHandler.java:307) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.java:56) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.handleMessage(ExceptionWebSocketHandlerDecorator.java:58) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.sockjs.transport.session.AbstractSockJsSession.delegateMessages(AbstractSockJsSession.java:382) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession.handleMessage(WebSocketServerSockJsSession.java:193) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler.handleTextMessage(SockJsWebSocketHandler.java:92) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.java:43) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:110) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:42) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:81) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:78) [spring-websocket-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:399) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:106) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:500) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:295) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:131) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:69) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148) [tomcat-embed-websocket-8.5.6.jar:8.5.6]
    at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:802) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1410) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_101]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_101]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.6.jar:8.5.6]
    at java.lang.Thread.run(Thread.java:745) [na:1.8.0_101]
    Caused by: org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-4.1.4.RELEASE.jar:4.1.4.RELEASE]
    at org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor.preSend(ChannelSecurityInterceptor.java:71) ~[spring-security-messaging-4.0.2.RELEASE.jar:4.0.2.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:158) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:113) ~[spring-messaging-4.3.5.RELEASE.jar:4.3.5.RELEASE]
    ... 30 common frames omitted 

Security Config:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserProfileService userProfileService;

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;

    @Autowired
    public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userProfileService);
        auth.authenticationProvider(authenticationProvider());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userProfileService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/javascript/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http    .httpBasic()
                    .and()
                .authorizeRequests()
                    .antMatchers("/", "/index.html", "/home.html", "/getLatestPost", "/application-socket-conn/**").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()
                .csrf().disable();
    }
}

Web Socket Config:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        //Javascript connection subscribes to this URI
        config.enableSimpleBroker("/main-page-feed");

        //STOMP messages are sent to this URI + suffix
        config.setApplicationDestinationPrefixes("/thought-bubble");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        //URI used for SockJS connection
        registry.addEndpoint("/application-socket-conn").withSockJS();
    }
}

Custom AccessDenied Handler (not catching it):

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    @Qualifier("clientOutboundChannel")
    private MessageChannel clientOutboundChannel;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Message<String> message = new Message<String>() {
            @Override
            public String getPayload() {
                return "Access denied.";
            }

            @Override
            public MessageHeaders getHeaders() {
                return null;
            }
        };

        clientOutboundChannel.send(message);
    }
}

Web Socket Controller

@RestController
public class WebSocketController {

    @Autowired
    private SimpMessagingTemplate simpMessagingTemplate;

    @MessageMapping("/push-to-queue")
    public void pushThoughtToQueue(@Payload ThoughtEntity entity) throws Exception {
        Date today = new Date(Calendar.getInstance().getTimeInMillis());

        entity.setPostDate(today);
        entity.setFavoriteCount(0);

        this.simpMessagingTemplate.convertAndSend("/main-page-feed/thought-queue", entity);
    }
}

I think AffirmativeBased is the class where this is getting thrown, but I'm not sure why, or why normal (non-web socket) spring security doesn't do this when it denies access to something. Like I said, it's correctly denying access, but it's throwing the runtime exception and sending the ugly stacktrace back to the client through the websocket.

UPDATE:

I've realized that the exception is thrown whether or not the user is logged in, so I'm thinking that this is a different issue. Like I said, I'm not using ROLEs for this application, and I think that has something to do with it. I'm currently researching anonymous roles because I think that's related to what's going on.

like image 241
Trevor Bye Avatar asked Oct 30 '22 13:10

Trevor Bye


1 Answers

it seems that your code of websocket client is not sending authorization information. Usually it is not handled by default, and you should create your own way to connect with security.

In my case I am using oauth authorization and have to specify specific header Authorization : Bearer _uuid_token_, which is specified during stompClient connection.

Have a look at this snippet to get general idea. (I am using AngularJS)

(function() {
    'use strict';
    /* globals SockJS, Stomp */

    angular
        .module('myApp')
        .factory('global_WebSocket', GlobalWebSocketClient);

    GlobalWebSocketClient.$inject = ['$window', 'localStorageService', '$q'];

    function GlobalWebSocketClient($window, localStorageService, $q) {

        var connected = $q.defer();

        var established = {established: false} ;

        var loc = $window.location;
        var url = loc.protocol + '//' + loc.host + loc.pathname + 'websocket';
        var token = localStorageService.get('token');
        if (token && token.expires_at && token.expires_at > new Date().getTime()) {
            url += '?access_token=' + token.access_token;
        } else {
            url += '?access_token=no token';
        }

        /*jshint camelcase: false */
        var socket = new SockJS(url);


        /*jshint camelcase: false */
        var stompClient = Stomp.over(socket);
        var headers = {
            Authorization : 'Bearer ' + token.access_token,
        };

        stompClient.debug = null;


        var establishConnection = function() {
            stompClient.connect(headers, function() {
                established.established = true;
                connected.resolve('success');
            }, function(error) {
                console.log("ERROR CONNECTNG!");
                console.log(error.headers);
                establishConnection();
            });
        };


        establishConnection();



        return {
            connected: connected,
            client: stompClient,
            established: established
        };
    }
})();

As you can see this code constructs url adding access_token as well as specifies Authorization headers, which were established earlier and have been stored in localStorageService. I guess you were assuming your client is sending the headers by default, but with sockjs it is not so.

Then I can create such client-service

(function() {
    'use strict';
    /* globals SockJS, Stomp */

    angular
        .module('myApp')
        .factory('synchronization_Status', SynchronizationFileTrackerService);

    SynchronizationFileTrackerService.$inject = ['global_WebSocket'];

    function SynchronizationFileTrackerService (global_WebSocket) {
        var stompClient = global_WebSocket;
        var subscriber = {} ;


        return {
            subscribe: subscribe,
            unsubscribe: unsubscribe
        };

        function unsubscribe(target) {
            if (subscriber[target]) {
                subscriber[target].unsubscribe();
            }
        };


        function subscribe(forTarget, handler) {
                if (stompClient.established.established) {
                    subscriber[forTarget] = stompClient.client.subscribe('/synchronization/status/' + forTarget, function(data) {
                        data = angular.fromJson(data.body);
                        handler(angular.fromJson(data));

                    });
                } else {
                    stompClient.connected.promise.then(
                        function() {
                            subscriber[forTarget] = stompClient.client.subscribe('/synchronization/status/' + forTarget, function(data) {
                                data = angular.fromJson(data.body);
                                handler(angular.fromJson(data));

                            });
                        }, null, null);
                }
        }
    }
})();

And reuse such service in my UI code as simple as:

synchronization_Status.subscribe(ctrl.id, funciton(response){ctrl.currentStatus = response} );
like image 157
Ilya Dyoshin Avatar answered Nov 15 '22 05:11

Ilya Dyoshin