How to correctly implement security on a websocket session? - spring-boot

I want to implement an asynchronous mechanism using websockets.
Here's the idea:
The client performs a REST call
The server returns a "subscribingID" and starts a background process
The client registers as subscriber on this topic (suppose 12232442 is the id):
this.stompClient.subscribe('/callback/12232442', (messageOutput) => {
let mess = JSON.parse(messageOutput.body);
console.log(mess);
});
Once done the server simply sends the message and closes the connection:
stompSession.send("callback/12232442", new MessageOutput());
It should work but here's the catch: how can I be sure that another client can't simply subscribe to an ID that exists but does not belong to them?
Also, is there any built-in mechanism to achieve this?

When the server receives a REST request for a subscription ID, you can store the newly generated ID in a Subscription HashMap.
In order to do processing when a new subscription request comes you can implement a custom StompEventHandler, like so
#Controller
public class StompEventHandler{
#EventListener
public void handleSubscription(SessionSubscribeEvent event) {
//Get incoming sessionDetails from event.
//get the destination.
// Validate that the destination is present in Subscription HashMap
// and also that no client maps to the topic id.
// Based on the result either send the message or send Unauth message to
client.
}
}
Documentation
Note that you have to store details about session ID of the client as well for this. Instead of broadcasting the message to /topic/callback/<your_id>, you would need to send the message to destination like so: /user/queue/callback/<your_id>. For sending to a destination as such you would need to use simpMessagingTemplate.convertAndSendToUser(username, destination, payload, Headers)
Good Read for this
So since you are sending messages to only a particular session of a particular user, your messages are confidential.
If you want to ensure that you do not even have the subscription from the client you can send an UNSUBSCRIBE message to the client in the StompEventHandler class. This would force unsubscribe the client.
Good Read for this

Related

Spring websocket: how to send to all subscribers except the message sender

I am following the quick-start guide on Spring websocket with sockJs and Stomp here:
https://spring.io/guides/gs/messaging-stomp-websocket/
At this point, my code looks like to one from guide and works as intended. I have a controller class with a method accepting incoming messages and sending them back to all who subscribed on the topic.
What I want to do, is to change the code, so my #MessageMapping annotated method sends response to all subscribers excluding the one who send the message to the controller in the first place (because the sender is also subscribed to the same topic, but i dont want the sender to keep receiving messages it send itself, it is kind of a loop I guess).
I have seen many docs describing how to send a message to a single subscriber, but have not yet seen on describing how to send to all but one - the initial message sender.
Is there any built-in way to do this easily in Spring websocket?
Ok so i've managed to find some solution which works for me at this point of time:
i was able to filter subscribers by principal user name.
I got all simp users form org.springframework.messaging.simp.user.SimpUserRegistry,
and a current sender from org.springframework.messaging.simp.stomp.StompHeaderAccessor.
My code looks something like this:
#MessageMapping("/game/doStuff")
public void gameGrid(DoStuffMessage doStuffMessage,
StompHeaderAccessor headers) {
sendTo("/game/doStuff", doStuffMessage, headers);
}
private void sendTo(String destination, Object payload, StompHeaderAccessor headers) {
Optional<String> user = Optional.ofNullable(headers.getUser())
.map(Principal::getName);
if (user.isPresent()) {
List<String> subscribers = simpUserRegistry.getUsers().stream()
.map(SimpUser::getName)
.filter(name -> !user.get().equals(name))
.collect(Collectors.toList());
subscribers
.forEach(sub -> simpMessagingTemplate.convertAndSendToUser(sub, destination, payload));
}
}
Client is subscribing to /user/game/doStuff
It works for now. What I am worried about is if this code can scale horizontally - if someone has any insight on this I'd greatly appreciate that.

masstransit deferred respond in sagas

I am investigating using sagas in mass transit to orchestrate activities across several services. The lifetime of the saga is short - less than 2 seconds if all goes well.
For my use case, i would like to use the request/respond approach, whereby the client requests a command, the saga handles that command, goes through some state changes as messages are received and eventually responds to the first command that initiated the saga, at which point the client receives the response and can display the result of the saga.
From what i can see, by this point, the context is no longer aware of the initial request. How can I reply to a message that was received in this way? Is there something i can persist to the saga data when handling the first event, and use that to reply later on?
Thanks Alexey. I have realised that I can store the ResponseAddress and RequestId from the original message on the saga, and then construct a Send() later on.
Getting the response details from the original request
MassTransit.EntityFrameworkIntegration.Saga.EntityFramework
SagaConsumeContext<TSagaData, TMessage> payload;
if (ctx.TryGetPayload(out payload))
{
ResponseAddress = payload.ResponseAddress;
RequestId = payload.RequestId ;
}
Sending the response
var responseEndpoint = await ctx.GetSendEndpoint(responseAddress);
await responseEndpoint.Send(message, c => c.RequestId = requestId);
UPDATE: The documentation has been updated to include a more complete example.
Currently, the saga state machine can only do immediate response like this:
// client
var response = await client.Request(requestMessage);
// saga
During(SomeState,
When(RequestReceived)
.Then(...)
.Respond(c => MakeResponseMessage(c))
.TransitionTo(Whatever)
)
So you can respond when handling a request.
If you want to respond to something you received before, you will have to craft the request/response conversation yourself. I mean that you will have to have decoupled response, so you need to send a message and have a full-blown consumer for the reply message. This will be completely asynchronous business.

Does spring-websocket STOMP support SUBSCRIBE id?

The STOMP spec says that SUBSCRIBE MUST have id header.
https://stomp.github.io/stomp-specification-1.2.html#SUBSCRIBE_id_Header
SUBSCRIBE id Header
Since a single connection can have multiple open subscriptions with a
server, an id header MUST be included in the frame to uniquely
identify the subscription. The id header allows the client and server
to relate subsequent MESSAGE or UNSUBSCRIBE frames to the original
subscription. Within the same connection, different subscriptions MUST
use different subscription identifiers.
However, in spring's example https://spring.io/guides/gs/messaging-stomp-websocket/, it doesn't specify an id when subscribing destination.
function connect() {
var socket = new SockJS('/gs-guide-websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
In spring's API, the SimpMessageSendingOperations.convertAndSendToUser doesn't support an id header explicitly.
My question is how to specify id header when sending a message to client?
I don't think you can use a Subscription ID to send a message to a specific client. Stomp defines this ID and Spring's implementation uses it internally to create messages to every client subscribed to the destination address. Therefore, the Subscription ID is transparent in the Stomp communication... You can specify it in the client side or let Stomp JS (STOMP Over WebSocket) create a unique one.
If you subscribe to a destination prefixed with "/user/" and use org.springframework.messaging.simp.SimpMessagingTemplate#convertAndSendToUser or org.​springframework.​messaging.​simp.​annotation.SendToUser to send a message to a single client, what Spring does is register and create a subscription to a custom session based destination based on the original destination. In another words, from Spring's Javadoc:
When a user attempts to subscribe, e.g. to
"/user/queue/position-updates", the "/user" prefix is removed and a
unique suffix added based on the session id, e.g.
"/queue/position-updates-useri9oqdfzo" to ensure different users can
subscribe to the same logical destination without colliding.
When sending to a user, e.g.
"/user/{username}/queue/position-updates", the "/user/{username}"
prefix is removed and a suffix based on active session id's is added,
e.g. "/queue/position-updates-useri9oqdfzo".
See http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.html
See https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-user-destination
EDITED:
You can't use the subscription ID to send a message direct to it's subscribed client, but you can use the client's session ID. According to here, you could use the user's name to send him a message. But you would need authenticated sessions with a Principal on it. Or you can force the destination's session ID in the header of the message, avoiding the internal step to discover it, as shown here.
private void sendMessageToUser(String destinationSessionId, String message) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(destinationSessionId);
headerAccessor.setLeaveMutable(true);
messagingTemplate.convertAndSendToUser(destinationSessionId, "/subscribe/private", message, headerAccessor.getMessageHeaders());
}
Doing like this, without a Principal in the session, I couldn't use #SendToUser annotation.

How does Spring WebSocket send message to a specific user?

I am reading the book Spring in Action 4 to work with STOMP messaging over WebSocket.
Suppose the user destination prefix is set as "/user" as below:
registry.setUserDestinationPrefix("/user");
Then client subscribes to a destination with below JavaScript code:
stomp.subscribe("/user/queue/notifications", handleNotifications);
Then on the server, the actual destination that the client subscribes to should be derived from its session, maybe like this:
/queue/notifications-user6hr83v6t --- (1)
Then I use the SimpMessagingTemplate to send message to that user:
messaging.convertAndSendToUser( username, "/queue/notifications",
new Notification("You just got mentioned!"));
Then the message will be sent to destination like this:
/user/<username>/queue/notifications ---(2)
Well, the two destinations (1) and (2) look different, how could the message ever reach the client?
The path
/user/<username>/queue/notifications
seems to be the "logical" path which is used in documentation. It is also initially created with convertAndSendToUser method. It is then translated into a technical format which is done in UserDestinationMessageHandler class in this line
UserDestinationResult result = this.destinationResolver.resolveDestination(message);
eg.
Given the subscription:
stompClient.subscribe('/user/queue/reply', function (greeting) { ...
sending a message with
stompClient.send("/app/personal", ...
and intercepting it with
#MessageMapping("/personal")
public void personalMessage(SimpMessageHeaderAccessor headerAccessor, PoCRequestMessage message) {
SimpMessageHeaderAccessor ha = SimpMessageHeaderAccessor
.create(SimpMessageType.MESSAGE);
ha.setSessionId(headerAccessor.getSessionId());
ha.setLeaveMutable(true);
PoCReplyMessage reply = new PoCReplyMessage("Personal Message" + message.getName());
simpMessagingTemplate.convertAndSendToUser(headerAccessor.getSessionId(), "/queue/reply", reply, ha.getMessageHeaders());
}
the destination will be resolved as follows:
source destination: /user/zojdn53y/queue/reply
target destination: /queue/reply-userzojdn53y
this is how the final destination name is resolved.
The target destination is the real name of the queue that is created (at least as long an external message broker is used - didn't check this for a simple in-memory broker but I assume this would be the same).
One important thing to note is that when you want to use an unauthenticated user (most often scenario when experimenting with Websockets) you need to additionally put the message headers in convertAndSendToUser method - this is well described in
Spring WebSocket #SendToSession: send message to specific session

How to reply to unauthenticated user in Spring 4 STOMP over WebSocket configuration?

I'm experimenting with Spring 4 WebSocket STOMP application. Is there a way to reply to a single unauthenticated user on condition that each user has unique session ID? Right now I can only either broadcast a message or send it directly to an authenticated user.
#Controller
public class ProductController {
#MessageMapping("/products/{id}")
#SendTo("/topic") // This line makes return value to be broadcasted to every connected user.
public String getProduct(#DestinationVariable int id) {
return "Product " + id;
}
}
You can assign an anonymous identity to incoming users. There are two ways to do it.
One, you can configure a sub-class of DefaultHandshakeHandler that overrides determineUser and assigns some kind of identity to every WebSocketSession. This requires 4.0.1 by the way (currently build snapshots are available) that will be released on Monday Jan 23, 2014.
Two, the WebSocket session will fall back on the value returned from HttpServletRequest.getUserPrincipal on the handshake HTTP request. You could have a servlet Filter wrap the HttpServletRequest and decide what to return from that method. Or if you're using Spring Security which has the AnonymousAuthenticationFilter, override its createAuthentication method.
#SendToUser("/products") should result in a message to destination "/user/{username}/products". That message will be handled by the UserDestinationMessageHandler, which transforms the destination to "/products-user{sessionId}" and re-sends the message.
So I'm not quite sure what "/user/products-user0" is. It surprises me in two ways. First if it starts with "/user" then that's the destination before the transformation and should be followed by the user name (i.e. "/user/{username}/products").
The fact that it ends with "-user0" makes it look like the destination after the transformation but then it shouldn't start with "/user". In any case the 0, 1 in that case would be the WebSocket session id. What server is this?

Resources