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 ROLE
s 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.
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} );
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With