I am trying to use Websockets and STOMP 1.2. I want to authenticate user using JWT on CONNECT frame and to return error message if authorization is not valid. Here is channel interceptor
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer
{
// ... Removed for readability
@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 (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand()))
{
String token = accessor.getFirstNativeHeader(authorizationHeader);
if (token != null)
{
Authentication authentication = tokenUtil.getAuthentication(resolveToken(token));
accessor.setUser(authentication);
}
else
{
throw new UnauthorizedException();
}
}
return message;
}
});
}
}
This way the error stack will have
Failed to send client message to application via MessageChannel in session a63cf95f-4a5b-ef34-b74b-3264ec6dd61f. Sending STOMP ERROR to client.
org.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is com.example.error.exception.UnauthorizedException: You are not authorized. Please log in.
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:146) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:122) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:284) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.handleMessage(SubProtocolWebSocketHandler.java:324) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.java:56) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.handleMessage(ExceptionWebSocketHandlerDecorator.java:58) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:113) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:42) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:84) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:81) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:395) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:119) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:495) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:294) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:82) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:171) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:151) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.12.jar:9.0.12]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.12.jar:9.0.12]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
Caused by: com.example.error.exception.UnauthorizedException: You are not authorized. Please log in.
at com.exampleconfig.WebSocketConfig$1.preSend(WebSocketConfig.java:74) ~[classes/:na]
at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:178) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:132) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
... 28 common frames omitted
And similar error will be sent to client end connection will be closed. But after the connection is closed, other stack trace is written as output:
2018-11-11 01:06:48.077 DEBUG 7259 --- [nio-8080-exec-2] s.w.s.h.LoggingWebSocketHandlerDecorator : StandardWebSocketSession[id=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f, uri=/api/ws] closed with CloseStatus[code=1000, reason=null]
2018-11-11 01:06:48.077 DEBUG 7259 --- [nio-8080-exec-2] o.s.w.s.m.SubProtocolWebSocketHandler : Clearing session a63cf95f-4a5b-ef34-b74b-3264ec6dd61f
2018-11-11 01:06:48.084 DEBUG 7259 --- [nio-8080-exec-2] org.springframework.web.SimpLogging : Detected unsent DISCONNECT message. Processing anyway.
2018-11-11 01:06:48.085 DEBUG 7259 --- [nio-8080-exec-2] org.springframework.web.SimpLogging : Processing DISCONNECT session=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f
2018-11-11 01:06:48.086 DEBUG 7259 --- [tboundChannel-1] o.s.w.s.m.SubProtocolWebSocketHandler : No session for GenericMessage [payload=byte[0], headers={simpMessageType=DISCONNECT_ACK, simpDisconnectMessage=GenericMessage [payload=byte[0], headers={simpMessageType=DISCONNECT, stompCommand=DISCONNECT, simpSessionAttributes={org.springframework.messaging.simp.SimpAttributes.COMPLETED=true}, simpSessionId=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f}], simpSessionId=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f}]
2018-11-11 01:06:48.087 WARN 7259 --- [nio-8080-exec-2] w.s.h.ExceptionWebSocketHandlerDecorator : Unhandled exception after connection closed for ExceptionWebSocketHandlerDecorator [delegate=LoggingWebSocketHandlerDecorator [delegate=SubProtocolWebSocketHandler[StompSubProtocolHandler[v10.stomp, v11.stomp, v12.stomp]]]]
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:146) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:122) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.messaging.StompSubProtocolHandler.afterSessionEnded(StompSubProtocolHandler.java:611) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.clearSession(SubProtocolWebSocketHandler.java:516) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.afterConnectionClosed(SubProtocolWebSocketHandler.java:385) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.handler.WebSocketHandlerDecorator.afterConnectionClosed(WebSocketHandlerDecorator.java:85) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.afterConnectionClosed(LoggingWebSocketHandlerDecorator.java:72) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.afterConnectionClosed(ExceptionWebSocketHandlerDecorator.java:78) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.onClose(StandardWebSocketHandlerAdapter.java:144) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.apache.tomcat.websocket.WsSession.fireEndpointOnClose(WsSession.java:535) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.WsSession.onClose(WsSession.java:513) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.WsFrameBase.processDataControl(WsFrameBase.java:347) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:289) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:82) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:171) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:151) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415) [tomcat-embed-core-9.0.12.jar:9.0.12]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.12.jar:9.0.12]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.12.jar:9.0.12]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
Caused by: org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.1.1.RELEASE.jar:5.1.1.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.1.1.RELEASE.jar:5.1.1.RELEASE]
at org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor.preSend(ChannelSecurityInterceptor.java:69) ~[spring-security-messaging-5.1.1.RELEASE.jar:5.1.1.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:178) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:132) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
... 26 common frames omitted
I have no idea where AccessDeniedException
came from and how can I handle this.
If I do not throw UnauthorizedException
in preSend
method, client will be connected and when it tries to send something, it will receive AccessDeniedException
, because all endpoints needs authenticated user, and then connection will be closed and stack trace for AccessDeniedException
printed few times.
What am I doing incorrect here and what is the correct way to handle this kind of use case?
As solution for AccessDenied exception is that I didn't configured to allow DISCONNECT messages to pass through security. This is my configuration now:
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer
{
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages)
{
messages
.simpTypeMatchers(SimpMessageType.CONNECT,
SimpMessageType.DISCONNECT, SimpMessageType.OTHER).permitAll()
.anyMessage().authenticated();
}
}
Also, I overrode StompSubProtocolErrorHandler
so I could send custom message to my client about error.
Here is example:
@Override
public Message<byte[]> handleClientMessageProcessingError(Message<byte[]>clientMessage, Throwable ex)
{
Throwable exception = ex;
if (exception instanceof MessageDeliveryException)
{
exception = exception.getCause();
}
if (exception instanceof UnauthorizedException)
{
return handleUnauthorizedException(clientMessage, exception);
}
if (exception instanceof AccessDeniedException)
{
return handleAccessDeniedException(clientMessage, exception);
}
return super.handleClientMessageProcessingError(clientMessage, ex);
}
...
private Message<byte[]> handleUnauthorizedException(Message<byte[]> clientMessage, Throwable ex)
{
ApiError apiError = new ApiError(ErrorCodeConstants.UNAUTHORIZED, ex.getMessage());
return prepareErrorMessage(clientMessage, apiError, ErrorCodeConstants.UNAUTHORIZED_STRING);
}
private Message<byte[]> prepareErrorMessage(Message<byte[]> clientMessage, ApiError apiError, String errorCode)
{
String message = transformApiErrorToJSONString(apiError);
StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.ERROR);
setReceiptIdForClient(clientMessage, accessor);
accessor.setMessage(errorCode);
accessor.setLeaveMutable(true);
return MessageBuilder.createMessage(message != null ? message.getBytes() : EMPTY_PAYLOAD, accessor.getMessageHeaders());
}