How to check if websocket user is authenticated when subscribe a channel? - java

I have websocket config class with :
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topics");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/notifications").setAllowedOrigins("*").withSockJS();
}
As well as ClientInboundChannel:
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
final String xAuthToken = accessor.getFirstNativeHeader(ManipulatesHeaders.X_AUTH_TOKEN_HEADER);
if (xAuthToken == null) {
return message;
}
final UserDetails userDetails = authService.getUserDetails(xAuthToken);
if (StompCommand.CONNECT == accessor.getCommand()) {
final WebSocketPrincipal principal = (...)
userRegistry.registerUser(principal, message);
accessor.setUser(principal);
}
return message;
}
}
}
Now, I would like to send a welcome messsage to every user that subscribe a particular channel. It's obvious that It might be achieved by creating a class that implements ApplicationListener<SessionSubscribeEvent> and provides
#Override
public void onApplicationEvent(final SessionSubscribeEventevent) {
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage());
}
Second approach is to perform similar thing in aforementioned ClientInboundChannel (it's the same).
The problem is, that there is no simpUser header in a message when a user send a stomp SUBSCRIBE.
Process is as follows: User sends stomp CONNECT, this line is executed: accessor.setUser(principal) and at this point message has properly setsimpUser header. But when stomp SUBSCRIBE message is received in preSend, the StompHeaderAccessor does not have simpUser header. Thus I am not able to asses whether user is already authenticated or not.
So how could I check if a user that sent a SUBSCRIBE message is already authenticated or not ? (and why authenticated user does not send simpUser header)

In your websocket configuration class change your extend to AbstractSecurityWebSocketMessageBrokerConfigurer
then you can set your security as follows:
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry message) {
message
.nullDestMatcher().permitAll()
.simpDestMatchers("/app/**").authenticated()
.simpSubscribeDestMatchers("/topics/**").authenticated()
.anyMessage().denyAll();
}
if you get the error Could not verify the provided CSRF token because your session was not found you will have to override `sameOriginDisabled in the websocket configuration class
#Override
protected boolean sameOriginDisabled() {
return true;
}
for more information on websocket security:
https://docs.spring.io/spring-security/site/docs/current/reference/html/websocket.html

Related

How can I add a Principal in configureClientInboundChannel?

I am using SockJS + STOMP + Spring WebSocket with a configuration that receives user credentials during the handshake. The credentials are received in the configureClientInboundChannel method:
#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())
|| StompCommand.SEND.equals(accessor.getCommand()))) {
List<String> auth = accessor.getNativeHeader("Authorization");
System.out.printf("Authorization: %s%n", auth.get(0));
}
return message;
}
});
}
I would like to add a Principal based on the token I receive in the header. I have a handler that registers Principals:
public class PrincipalHandshakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler handler, Map<String, Object> attributes) {
System.out.println(attributes);
return new Principal() {
#Override
public String getName() {
return userId;
}
};
}
}
But how do I get pass the token I get in configureClientInboundChannel to the above handshake handler? Basically, the token should go there in place of userId.

Unable to Send Binary Data (byte[]) using SimpMessagingTemplate

I am able to send string messages to websocket using SimpMessagingTemplate.convertAndSend() method, but same is not working when I try to send byte[]. When I send byte[] to the websocket subscription channel, a websocket DISCONNECT event is getting triggered and connection is getting lost. Any Idea to send byte[] to websocket using SimpMessagingTemplate !!!!!
#Autowired
private SimpMessagingTemplate template;
String body = "Message to be Sent";
template.convertAndSend("/channel", body); --------- working
template.convertAndSend("/channel", body.getBytes()); --------- Not working
Have you tried adding a custom message converter to the byte array?. Just override configureMessageConverters method.
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages.simpTypeMatchers(
SimpMessageType.DISCONNECT, SimpMessageType.OTHER).permitAll();
messages.anyMessage().authenticated();
}
#Override
public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
messageConverters.add(new ByteArrayMessageConverter());
return false;
}
#Override
protected boolean sameOriginDisabled() {
return true;
}
}

Why can I subscribe to destination but I don't receive any messages for that user with Spring WebSocket STOMP (with Spring Security and Keycloak)?

I'm trying to implement a microservice for sending messages via WebSocket.
I can send correctly messages to a client subcribed and authenticated (passing the JWT token to the WebClient server), but now I want send messages only to a particular user.
Following the official spring documentation I can have the client correctly subscribed but I don't get any message.
WebSocket Server configuration:
WebSecurityConfig.java
[...]
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.cors()
.and().csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
;
}
#Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/ws/**");
}
[...]
WebSocketConfig.java
#Slf4j
#Configuration
//#EnableScheduling
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Qualifier("websocket")
private AuthenticationManager authenticationManager;
#Autowired
public WebSocketConfig(AuthenticationManager authenticationManager)
{
this.authenticationManager = authenticationManager;
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*")
registry.addEndpoint("/ws").setAllowedOrigins("*").withSockJS();
}
#Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
#Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor headerAccessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(headerAccessor.getCommand()))
{
// log.info("TOKEN: {}", headerAccessor.getNativeHeader("token").toString());
Optional.ofNullable(headerAccessor.getNativeHeader("token")).ifPresent(ah ->
{
String bearerToken = ah.get(0).replace("Bearer ", "");
JWSAuthenticationToken token = (JWSAuthenticationToken) authenticationManager
.authenticate(new JWSAuthenticationToken(bearerToken));
headerAccessor.setUser(token);
});
}
return message;
}
});
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic", "/queue");
}
}
WebSocketSecurityConfig.java
#Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpTypeMatchers(CONNECT, HEARTBEAT, UNSUBSCRIBE, DISCONNECT).permitAll()
.simpDestMatchers("/app/**", "/topic/**", "/queue/**").authenticated()
.simpSubscribeDestMatchers("/topic/**", "/queue/**", "/user/**").authenticated()
.anyMessage().denyAll();
}
#Override
protected boolean sameOriginDisabled() {
//disable CSRF for websockets for now...
return true;
}
}
In a Java class I tryied both convertAndSend and convertAndSendToUser (probably a better choice) functions for send the message:
simpMessagingTemplate.convertAndSend("/user/" + username + "/queue/private-messages", message);
simpMessagingTemplate.convertAndSendToUser("user","/queue/private-messages", message);
WebSocket Client configurations
WebSecurityConfig.java
[...]
#Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.cors()
.and().csrf().disable()
.authorizeRequests()
.anyRequest()
.authenticated()
;
}
[...]
StompClient.java where i use the function createAndConnectClient for create the connection
[...]
public void createAndConnectClient(String accessToken) {
WebSocketClient client = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(client);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
StompSessionHandler sessionHandler = new MyStompSessionHandler(stompConfig);
// connect with custom headers
final WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
final StompHeaders head = new StompHeaders();
head.add("token", accessToken);
stompClient.connect(serverURL, headers, head, sessionHandler);
}
[...]
MyStompSessionHandler
#Component
public class MyStompSessionHandler extends StompSessionHandlerAdapter {
private final StompConfig stompConfig;
#Autowired
public MyStompSessionHandler(StompConfig stompConfig) {
this.stompConfig = stompConfig;
}
#Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
log.info("New session established : " + session.getSessionId());
session.subscribe("/user/queue/private-messages", this);
log.info("Subscribed to /user/queue/private-messages");
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
log.error("Got an exception", exception);
}
#Override
public Type getPayloadType(StompHeaders headers) {
return Message.class;
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
if (payload != null) {
Message msg = (Message) payload;
log.info("Received : " + msg.getText() + " from : " + msg.getFrom());
} else {
log.info("NULL Payload");
}
}
}
As I said I can subscribe to the destination but I don't receive any message.
EDIT: I added this code when I send the message
[...]
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
JWSAuthenticationToken token = null;
try {
token = userService.getToken(message.getTo());
log.info("TOKEN BEFORE SEND: {}", token);
} catch (NotFoundException e) {
e.printStackTrace();
log.error("User isn't connected");
}
headerAccessor.setUser(token);
headerAccessor.setSessionId(userService.getSessionId(message.getTo()));
log.info("SESSION-ID: {}", headerAccessor.getSessionId());
log.info("HEADER: {}", headerAccessor.toString());
simpMessagingTemplate.convertAndSendToUser(message.getTo(), webSocketConfig.getDestination(), message, headerAccessor.getMessageHeaders());
[...]
and so... I'm sure the user (and the SessionID too) is the same. but still not receinving nothing on my client!
WORKING SOLUTION:
Not the thing I want but... it works! ¯_(ツ)_/¯
(no benefit from STOMP mechanisms for users management)
When I subscribe to destination, in my Client, I add to destionation url the name of the user
"/queue/messages-" + user.getName().
Using convertAndSend on that destination (Server side that send messages and without headers) I can send private messages to user.
I believe this is happening because the DefaultSimpUserRegistry is not getting updated with the user. If you see this line where it is supposed to add the user, it does not add from the STOMP headerAccesssor but from the from the sub-protocol i.e websocket protocol. So you will have to set the user for the websocket session.
private void populateSecurityContextHolder(String
username, List<SimpleGrantedAuthority> grantedAuthorities) {
PreAuthenticatedAuthenticationToken authToken = new PreAuthenticatedAuthenticationToken(
username, null, grantedAuthorities);
SecurityContextHolder.getContext().setAuthentication(authToken);
}
So on setting this, the user registry should get updated with the user and hence when the message is sent to the queue when it finds the user it should be able to send it to the user, rather than discarding the message because the user was not present.
Did you set the UserDestinationPrefix for SimpMessagingTemplate. If not can you please try setting that to '/user & see if it works.
Not the thing I want but... it works! ¯_(ツ)_/¯
(no benefit from STOMP mechanisms for users management)
When I subscribe to destination, in my Client, I add to destionation url the name of the user:
"/queue/messages-" + user.getName().
Using convertAndSend on that destination (Server side that send messages) I can send private messages to user.

How to prevent subscription to spring socket /queue/private/* destination.

I have an Java socket API application, that handles socket requests from users and sends responses.
I have a configurer:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static final Logger LOGGER = Logger.getLogger(WebSocketConfig.class);
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue");
config.setApplicationDestinationPrefixes("/server_in");
config.setUserDestinationPrefix("/user");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket").withSockJS();
}
}
When i send response to user i do the following:
this.simpMessagingTemplate.convertAndSend("/queue/private/user_"+secret_key, socketResponse);
On client i have the following code:
sc.subscribe('/queue/private/user_'+secret_key, function (greeting) {
console.log(greeting.body);
});
And the response is handled successfully.
But the problem is that some other user can also subscribe to "/queue/private/*" destination and handle private messages.
sc.subscribe('/queue/private/*', function (greeting) {
console.log(greeting.body);
});
How can I privent that behaviour?
If you want each user to have a socket and only get him messages, what you can do is :
Subscribe as you do to the endPoint but with "/user" infront for example
sc.subscribe('/user/queue/websocket, function (greeting) {
console.log(greeting.body);
});
and at the server side you should have a rest method:
#RequestMapping(value = "/test", method = RequestMethod.POST)
public void test(Principal principal) throws Exception {
this.template.convertAndSendToUser(principal.getName(), "/queue/click", "");
}
With this every user subscibers to each channel and only the user is notified about, when a rest call is made.
The rest call should be authenticated so the Principal has the username.
The user channel is auto managed from Spring so you have to add it like this.
You can extend ChannelInterceptorAdapter and manage each event individually:
public class AuthorizedChannelInterceptorAdapter extends ChannelInterceptorAdapter {
#Override
public Message<?> preSend(Message<?> message, MessageChannel messageChannel) throws AuthenticationException {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT == accessor.getCommand())
setUserAuthenticationToken(accessor);
else if (StompCommand.SUBSCRIBE == accessor.getCommand())
validateSubscription((Authentication) accessor.getUser(), accessor.getDestination());
return message;
}
private void setUserAuthenticationToken(StompHeaderAccessor accessor) {
String token = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
accessor.setUser(loadAuthentication(token));
}
private Authentication loadAuthentication(String token){
return ....;
}
private void validateSubscription(Authentication authentication, String destination) {
if(...)
throw new AccessDeniedException("No permission to subscribe to this topic");
}
}
First of all you will need to store the authentication object provided by the client in connection event. After this, each event sent by the client will have this authentication object set so you can use it to validate if it is authorized to subscribe to a particular channel.

How to send a message through web socket to a connected user?

I want to send a message through a web socket to a specific user. So far I can open a web socket and read message from client like that:
#ServerEndpoint(value = "/wsep")
public class WebSocketEndpoint {
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketEndpoint.class);
private Session session;
#OnOpen
public void onOpen(Session session) {
this.session = session;
try {
session.getBasicRemote().sendText("You are connected. Your ID is " + session.getId());
} catch (Exception e) {
LOGGER.error("Error on open web socket", e);
}
}
#OnMessage
public void onClientMessage(String message, Session session) {
LOGGER.info("Message from {} is: {}", session.getId(), message);
}
#OnClose
public void onClose(Session session) {
this.session = null;
LOGGER.info("{} disconnected", session.getId());
}
}
I have an independent service which creates message in destination to a user. My Message class is a simple POJO:
public class Message {
private String fromUserName;
private String toUserName;
private String content;
...
}
When a new message is created in my MessageService, I want to inform the receiver if he is connected. I think I have to add a method WebSocketEndpoint.onServerMessage:
public void onServerMessage(Session session, Message message) {
session.getBasicRemote().sendText(message.getContent());
}
But I don't know how to do something like that which works.
There will be one instance of the ServerEndpoint for all your users. So, it should store all client sessions. As Vu.N suggested, one way you can do it is using a map:
Map<String, Session> sessions = new ConcurrentHashMap<>();
public void onOpen(Session session) {
String username = [...]
sessions.put(username, session);
}
Then, it will be easy to send the message to the user:
public void onServerMessage(Session session, Message message) {
sessions.get(message.getToUserName())
.getBasicRemote() // see also getAsyncRemote()
.sendText(message.getContent());
}
Now the hardest part is to get the username?
In my past works, I've done it in 3 ways:
The client connects to a URL that has some "key". This "key" would be used to find the right username. The WebSocket server endpoint would be like this:
#ServerEndpoint(value="/wsep/{key}") // the URL will have an extra "key"
public class WebSocketEndpoint {
[...]
#OnOpen
public void onOpen(Session session, #PathParam("key") String key) {
String username = getUserNameWithKey(key);
sessions.add(username, session);
}
The client sends some information in the first message. You just ignore the #OnOpen part:
#OnOpen
public void onOpen(Session session) {
LOGGER.info("Session open. ID: {}", session.getId());
}
#OnMessage
public void onMessage(Session session, String message) {
String username = getUserNameFromMessage(message);
sessions.add(username, session);
}
Some user info can be obtained from a cookie, JWT, or something. You'll need a ServerEndpointConfig.Configurator to get that information from the request. For example:
public class CookieServerConfigurator extends ServerEndpointConfig.Configurator {
#Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
Map<String,List<String>> headers = request.getHeaders();
sec.getUserProperties().put("cookie", headers.get("cookie"));
}
}
Then, the server endpoint will point to that configurator:
#ServerEndpoint(value = "/example2/", configurator = CookieServerConfigurator.class)
public class WebSocketEndpoint {
and you can get the information like this:
#OnOpen
public void onOpen(Session session, EndpointConfig endpointConfig) {
String username = getUsername((List<String>)endpointConfig.getUserProperties().get("cookie"));
You can see some working examples here: https://github.com/matruskan/websocket-example
For more complex systems, you can also have a "tagging system", instead of using a Map. Then, each session can receive messages sent to any of its tags, and a message sent to a tag can be directed to many sessions.

Categories