blackhorse-one / stomp_dart_client

Dart STOMP client for easy messaging interoperability.
BSD 3-Clause "New" or "Revised" License
59 stars 45 forks source link

SecuredWebSockets Access Denied Only On Web #84

Closed tjmeal closed 1 year ago

tjmeal commented 2 years ago

Hello,

I do have an implementation which works on ios/android but when running it on web i get access denied error

"<<< a["ERROR\nmessage:Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.access.AccessDeniedException\c Access is denied\ncontent-length:0\n\n\u0000"]"

Bellow is my implementation :

final stompClient = StompClient( config: StompConfig.SockJS( url: '$BASE_ROUTE_ENDPOINT/websocket', onConnect: onConnect, webSocketConnectHeaders: {'Authorization': 'Bearer $_jwtToken'},

this in my case looks obsolete: Where it is used ? stompConnectHeaders: {'Authorization': 'Bearer $_jwtToken'},

// heartbeatIncoming: Duration(seconds: 1),
// heartbeatOutgoing: Duration(seconds: 1),
//connectionTimeout: Duration(seconds: 15),
reconnectDelay: Duration(seconds: 2),

onUnhandledFrame: (StompFrame frame) =>
    print('onUnhandledFrame :: ${frame.body}'),
onUnhandledMessage: (StompFrame frame) =>
    print('onUnhandledMessage :: ${frame.body}'),
onUnhandledReceipt: (StompFrame frame) =>
    print('onUnhandledReceipt :: ${frame.body}'),

onStompError: (StompFrame frame) => print('onStompError :: ${frame.body}'),
onWebSocketError: (dynamic error) =>
    print('onWebSocketError ::' + error.toString()),

onWebSocketDone: onWebSocketDone,
onDisconnect: (StompFrame frame) => print('onDisconnect :: ${frame.toString()}'),

onDebugMessage: (dynamic frame) =>
    print('onDebugMessage :: ${frame.toString()}'),

), );

Thanks in advance and i will be waiting your reply with any thoughts, because i have no idea what might be the problem...

KammererTob commented 2 years ago

Hi,

i am not sure what might be the issue from the code you provided. But please note that webSocketConnectHeaders are not supported in web. I suggest you move your authorization to the stompConnectHeaders.

The difference between webSocketConnectHeaders and stompConnectHeaders is where they are used. The former are used when the WebSocket connection is being established, while the latter are being used when the STOMP protocol does its connect handshake (this is not defined in the spec and needs some custom code in Spring)

tjmeal commented 1 year ago

Hello and thanks for your reply,

So since i need webSocketConnectHeaders to establish the connection and its not supported in web there is no other way right? i had tried stompHeaders but the connection can't get established Is the any planning for webSocketConnectHeaders to be supported on web too ?

KammererTob commented 1 year ago

Hey,

this is not a restriction of this library, but a restriction of WebSockets in the JavaScript WebSockets API. If you want to use stompConnectHeaders you need to have some additional code on your server, i.e. something like this (in your WebSocketMessageBrokerConfigurer):

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (accessor != null ) {
                    if (SimpMessageType.CONNECT.equals(accessor.getMessageType())) {
                        Optional.ofNullable(accessor.getNativeHeader("Authorization")).ifPresent(ah -> {
                            String bearerToken = ah.get(0).replace("Bearer ", "");
                            try {
                                Authentication authentication = authService.getAuthentication(bearerToken);
                                accessor.setUser(authentication);
                            } catch (Exception e) {
                            }
                        });
                    }
                }
                return message;
            }
        });
    }
tjmeal commented 1 year ago

Hey again,

I would like for starters to thank you for effort in any case !!

But i am not very familiar with the internals, but with few i know.

First i need to make a connection (http call to connect to the channel) and then send stompHeaders.

If i remove websocketHeaders and i am using only stompHeader > i am getting the following error:

Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied

from my public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer.

By your suggestion to use only using stompHeaders > if i understood correctly i should leave open the web-socket connection and authenticate on configureClientInboundChannel ?

If thats what you mean is that even secure ?

Thanks again and i will be waiting your reply.

KammererTob commented 1 year ago

Hey,

yes my suggestion would be to use stompConnectHeaders and implement a custom interceptor, which bascially is inspecting the headers of every CONNECT message send by the Stomp protocol. If a authorization header is present it uses it to authorize this connect request, if not it throws an exception (my example above catches it, but you would need to throw one). That way the stomp connection is only established if it has a Bearer token.

Regarding security: Yes. This should be secure, as long as their is no other operation possible on the active WebSocket connection apart from using it for the Stomp protocol.

tjmeal commented 1 year ago

Hello and i can't thank you enough for your help.

One last connection here, and i am asking because it looks you have been there already :)

Is the a way on which i can disable any other operation on the active Web-socket connection except stomp protocol ?

Thanks again.

KammererTob commented 1 year ago

Is the a way on which i can disable any other operation on the active Web-socket connection except stomp protocol ?

As far as i know there is no action possible, if you don't provide any Controllers working on the raw Websocket. The only security concern could maybe be that someone is able to open many WebSocket connections and effectively DDoS your server, but the same is true if you do Authorization on the WebSocket.

tjmeal commented 1 year ago

Same stance for authorization of http requests too then, So you probably right.

Thanks again for you time !! Have a great day.

tjmeal commented 1 year ago

Hello and sorry for bothering you again.

I find an issue maybe you have encounter that yourself, the code you sended to me in order to use stompHeader for Auth it connect successfully but on subscribe the user is null on the accessor.

@SubscribeMapping("/user/queue/transfers")
public WsTransferResponse subscribe() {
    return WsTransferResponse.builder()
            .transfers(transferFacade.findAllTransfersToOrganize())
            .action(WsActionEnum.SUBSCRIBE)
            .build();
}

I even changed the code to include Subscription but still the user is not in the context > i read something that @SubscribeMapping skips messageBroker does that have something to do with it ?

` @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new ChannelInterceptor() { @Override public Message<?> preSend(@Nonnull Message<?> message, @Nonnull MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

            if (accessor != null) {
                if (SimpMessageType.CONNECT.equals(accessor.getMessageType()) || SimpMessageType.SUBSCRIBE.equals(accessor.getMessageType())) {
                    Optional.ofNullable(accessor.getNativeHeader("Authorization")).ifPresent(ah -> {
                        String bearerToken = ah.get(0).replace("Bearer ", "");
                        try {
                            if (StringUtils.hasText(bearerToken) && tokenProvider.validateToken(bearerToken)) {
                                Authentication authentication = tokenProvider.getAuthentication(bearerToken);
                                accessor.setUser(authentication);
                            }
                        } catch (Exception e) {
                            throw new CustomException("Error With Stomp Connection");
                        }
                    });
                }
            }
            return message;
        }
    });
}`
KammererTob commented 1 year ago

Hey,

in your config that extends AbstractSecurityWebSocketMessageBrokerConfigurer, did you configure you configureInbound? Something like:

    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
            .simpTypeMatchers(CONNECT, UNSUBSCRIBE, DISCONNECT, HEARTBEAT).permitAll()
            .simpDestMatchers("/topic/public/**", "/queue/public/**").permitAll()
            .simpDestMatchers("/**").authenticated()
                .anyMessage().denyAll();
    }
tjmeal commented 1 year ago

Nop i had delete completely that class 👯 Do you have any idea why that happened only on Subscription was indeed about @SubscribeMapping ?

Now works btw, i will dig in to find a solution to have and when i do i'll let you know that the least i can do :D

I found "A common pattern for achieving WebSocket authentication/authorization is to implement a ticketing system where the page hosting the WebSocket client requests a ticket from the server and then passes this ticket during WebSocket connection setup either in the URL/query string, in the protocol field, or required as the first message after the connection is established. "

from here https://stackoverflow.com/questions/4361173/http-headers-in-websockets-client-api. but no idea how to implement it yet :P