Spring WebSocket ConvertAndSendToUser not working but convertAndSend working - java

I am new to Spring boot websocket and messaging semantics. Currently i am able to send private messages using the below code.
String queueName = "/user/" + username + "/queue/wishes";
simpMessagingTemplate.convertAndSend(queueName, message);
When trying to use convertAndSendToUser I am not getting any error but the message is not getting sent. I knew that with sendToUser there should be a slight change in how the destination name should be formed but I am not getting it right.
String queueName = "/user/queue/wishes";
simpMessagingTemplate.convertAndSendToUser(username, queueName, message);
Below is my subscription code.
stompClient.subscribe('/user/queue/wishes', function(message) {
alert(message);
})

I had a similar problem, if your username is actually a sessionId, then try to use one of the overloaded methods that accept headers (so said in SimpMessageSendingOperations javadoc):
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(username);
headerAccessor.setLeaveMutable(true);
messagingTemplate.convertAndSendToUser(username, "/queue/wishes", message, headerAccessor.getMessageHeaders());
my example

It is not getting sent because your "destination" i.e "queuename" is incorrect.
As you can see here SimpMessagingTemplate.java#L230 this is the method that gets invoked as a part of the chain on invocations of template.convertAndSendToUser().
This method already prefixes /user to the final destination. So instead you should do something like this:
simpMessagingTemplate.convertAndSendToUser(username,"/queue/wishes", message);
Now with the correct destination it should get sent to user.

Finally figured out the problem. I did a silly mistake. I was filling the User DestinationPrefix as part of WebSocket config. but didn't set it up for the injected bean SimpMessaging template.

You must registry the prefix of SimpleBroker:
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic", "/queue", "/user");
registry.setUserDestinationPrefix("/user");
}
Although the UserDestinationPrefix has been set for default value "/user/", you must to add the SimpleBroker prefix.

Related

Quarkus: how to secure WebSocket when using Keycloak / OIDC

I have gone down many rabbit holes and cannot get this working. I am hoping someone can help me.
I am using Keycloak and my REST endpoints are successfully secured like this abbreviated example:
#Path("/api")
public class MyResource {
#Inject
SecurityIdentity securityIdentity;
#Inject
JsonWebToken jwt;
#GET
#Path("/mydata")
#RolesAllowed("user")
#NoCache
public Uni<Response> getMyData(Request request) {
// Get a claim from the Keycloak JWT
String mySpecialClaim = (String) jwt.claim("myCustomClaim").get();
// Do some work...
String resJson = "{result of work here}";
return Uni.createFrom().item(resJson)
.onItem()
.transform(item -> item != "" ? Response.ok(item) : Response.status(Response.Status.NO_CONTENT))
.onItem()
.transform(Response.ResponseBuilder::build);
}
}
The access token is supplied by the client app which manages the Keycloak authentication and which sends the API request with a Bearer token. Standard stuff, all working. :-)
Now, I want to do something similar with a WebSocket endpoint.
I am using the Quarkus Websockets sample as my guide and can get it all working without Authorization - ie making unsecured calls.
I am stuck trying to secure the WebSocket connection.
The closest I have come to finding a solution is this post in the Quarkus GitHub issues:
https://github.com/quarkusio/quarkus/issues/29919
I have coded that up as per the sample code in the post. Logging shows the reactive route and WebSocketSecurityConfigurator are both being called and the access_token from the JS WebSocket client is present and presumably being processed by the Quarkus default security processes, as it does for REST end points. All good.
The missing piece is how to code the onOpen() and onMessage() methods in my WebSocket endpoint so they are secure, reactive, and I can access the JWT to get the claims I need.
Can anyone elaborate on this code fragment from the Quarkus issue post mentioned above, please? I have added what I think I need as per the sample further below.
The fragment from the issue post:
#Authenticated
#ServerEndpoint(
value ="/ws",
configurator = WebSocketSecurityConfigurator.class
)
public class WebSocket {
#Inject
UserInfo userInfo;
// ...
}
My additions:
#Authenticated
#ServerEndpoint(
value ="/services/{clientid}",
configurator = WebSocketSecurityConfigurator.class
)
public class WebSocket {
#Inject
SecurityIdentity securityIdentity;
#Inject
JsonWebToken jwt;
#Inject
UserInfo userInfo;
#OnOpen
#RolesAllowed("user") // Is this possible here? Or do I use the JWT and test myself?
public void onOpen(Session session, #PathParam("clientid") String clientid) {
// Get a claim from the Keycloak JWT
String mySpecialClaim = (String) jwt.claim("myCustomClaim").get();
// Do some setup work...
// eg cache the session in a map, etc
}
#OnMessage
public void onMessage(String message, #PathParam("clientid") String clientid) {
// Get a claim from the Keycloak JWT
String myOtherSpecialClaim = (String) jwt.claim("myOtherCustomClaim").get();
// Do some work using the message...
String someMessage = "tell the world";
// Broadcast something ...
myBroadcastFunction(someMessage);
}
}
In the non-secure version, the onOpen() and onMessage() methods return void because, of course, unlike a REST endpoint, one broadcasts the result instead of returning it.
In this secured version that does not work. If I only have an onOpen() method, and code it like this:
#OnOpen
public void onOpen(Session session, #PathParam("clientid") String clientid) {
Log.info("websocket onOpen session=" + session.getId());
}
It throws:
Unhandled error in annotated endpoint org.flowt.orgserver.gateway.WebSocketGateway_Subclass#732f20f8
java.lang.RuntimeException: java.lang.RuntimeException: java.lang.RuntimeException:
io.quarkus.runtime.BlockingOperationNotAllowedException: Blocking security check attempted in code running on the event loop.
Make the secured method return an async type, i.e. Uni, Multi or CompletionStage,
or use an authentication mechanism that sets the SecurityIdentity in a blocking manner prior to delegating the call
I would like not to block the event loop, so the first suggestion is the preferred one.
But how should I code that?
If I make the onOpen() return a Uni, how do I subscribe to it so it runs?
Can I still access the JWT to get the claims I need?
Do annotations like #RolesAllowed("user") still work in this context?
I wont waste space here with all my failed attempts. I am thinking I am not the first person to need to do this and there must be some kind of pattern to implement. The Quarkus docs are silent on this.
Can anyone tell me how to code the onOpen() and onMessage() methods using Quarkus so that the WebSocket endpoints are secured and the JWT is available inside those methods?
EDIT =======
To resolve the blocking exception, the Quarkus docs here say
To work around this you need to #Inject an instance
of io.quarkus.security.identity.CurrentIdentityAssociation,
and call the Uni<SecurityIdentity> getDeferredIdentity(); method.
You can then subscribe to the resulting Uni and will be
notified when authentication is complete and the identity
is available.
I cannot work out how to implement that instruction. Debugging into the Quarkus code I see that my access_token is being processed, the user is retrieved from Keycloak but the deferredIdentity is not being set. Therefore onOpen() never runs.
Clearly this is not what the docs mean me to do!
Here is my class:
package org.flowt.orgserver.gateway;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.websocket.Session;
import javax.websocket.OnOpen;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import io.quarkus.logging.Log;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.CurrentIdentityAssociation;
import io.quarkus.security.identity.SecurityIdentity;
import io.smallrye.mutiny.Uni;
#ApplicationScoped
#Authenticated
#ServerEndpoint(value = "/services/{clientid}", configurator = WebSocketSecurityConfigurator.class)
public class WebSocketGateway {
#Inject
SecurityIdentity securityIdentity;
#Inject
CurrentIdentityAssociation identities;
#OnOpen
public Uni<Void> onOpen(Session session, #PathParam("clientid") String clientid) {
// This never runs
Log.info("=========================== onOpen session=" + session.getId());
return identities.getDeferredIdentity()
.onItem()
.transformToUni(identity -> {
// Just to see if we reach here
Log.info("identity=" + identity.toString());
return Uni.createFrom().voidItem();
});
}
}
And just to reiterate: the REST endpoints in this same app for the same logged in user work perfectly.
Thanks,
Murray

Make asynchronous SOAP call in Spring WebFlux

I have a Reactive Spring Application using WebFlux with a REST API. Whenever a user calls my API, I need to make a call to a SOAP service which exposes a WSDL, perform some operation and return the result.
How do I combine this call to a SOAP service with the Reactive WebFlux framework?
The way I see it, I can do it 2 different ways:
Construct and send the SOAP message using WebFlux' WebClient.
Wrapping a synchronous call using WebServiceGatewaySupport in a Mono / Flux.
The first approach has my preference, but I don't know how to do that.
Similar questions have been asked here:
Reactive Spring WebClient - Making a SOAP call, which refers to this blog post (https://blog.godatadriven.com/jaxws-reactive-client). But I could not get that example to work.
Using wsdl2java in a Gradle plugin I can create a client interface with asynchronous methods, but I don't understand how to use this. When using the WebServiceGatewaySupport I don't use that generated interface or its methods at all. Instead, I call the generic marshalSendAndReceive method
public class MySoapClient extends WebServiceGatewaySupport {
public QueryResponse execute() {
Query query = new ObjectFactory().createQuery();
// Further create and set the domain object here from the wsdl2java generated classes
return (QueryResponse) getWebServiceTemplate().marshalSendAndReceive(query);
}
}
Can anyone share a complete example going from a WebFlux controller to making a SOAP call and returning asynchronously? I feel like I am missing something crucial.
I had the same aim but without having WSDL file. As an input I had endpoint and XSD file that defines request's scheme that I should to send. Here is my piece of code.
First let's define our SOPA WebClient bean (to avoid creating it each time when we want to make a call)
#Bean(name = "soapWebClient")
public WebClient soapWebClient(WebClient.Builder webClientBuilder) {
String endpoint = environment.getRequiredProperty(ENDPOINT);
log.info("Initializing SOAP Web Client ({}) bean...", endpoint);
return webClientBuilder.baseUrl(endpoint)
.defaultHeader(CONTENT_TYPE, "application/soap+xml")
//if you have any time limitation put them here
.clientConnector(getWebClientConnector(SOAP_WEBCLIENT_CONNECT_TIMEOUT_SECONDS, SOAP_WEBCLIENT_IO_TIMEOUT_SECONDS))
//if you have any request/response size limitation put them here as well
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs()
.maxInMemorySize(MAX_DATA_BUFFER_SIZE))
.build())
.build();
}
public static ReactorClientHttpConnector getWebClientConnector(int connectTimeoutSeconds, int ioTimeoutSeconds) {
TcpClient tcpClient = TcpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutSeconds * 1000)
.doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(ioTimeoutSeconds))
.addHandlerLast(new WriteTimeoutHandler(ioTimeoutSeconds)));
return new ReactorClientHttpConnector(HttpClient.from(tcpClient));
}
And now you can use the client to make SOAP calls like this:
#Slf4j
#Component
public class SOAPClient {
private final WebClient soapWebClient;
public SOAPClient(#Qualifier("soapWebClient") WebClient soapWebClient) {
this.soapWebClient = soapWebClient;
}
public Mono<Tuple2<HttpStatus, String>> send(String soapXML) {
return Mono.just("Request:\n" + soapXML)
.doOnNext(log::info)
.flatMap(xml -> soapWebClient.post()
.bodyValue(soapXML)
.exchange()
.doOnNext(res -> log.info("response status code: [{}]", res.statusCode()))
.flatMap(res -> res.bodyToMono(String.class)
.doOnNext(body -> log.info("Response body:\n{}", body))
.map(b -> Tuples.of(res.statusCode(), b))
.defaultIfEmpty(Tuples.of(res.statusCode(), "There is no data in the response"))))
.onErrorResume(ConnectException.class, e -> Mono.just(Tuples.of(SERVICE_UNAVAILABLE, "Failed to connect to server"))
.doOnEach(logNext(t2 -> log.warn(t2.toString()))))
.onErrorResume(TimeoutException.class, e -> Mono.just(Tuples.of(GATEWAY_TIMEOUT, "There is no response from the server"))
.doOnEach(logNext(t2 -> log.warn(t2.toString()))));
}
}
An important thing to mention here is that your soapXML should be in the format that defined by SOAP protocol obviously. To be more specific the message at least should starts and ends with soap:Envelope tag and consist all other data inside. Also, pay attention what version of SOAP protocol you are about to use as it defines what tags are allowed to use within the envelop and what not. Mine was 1.1 and here is specification for it
https://www.w3.org/TR/2000/NOTE-SOAP-20000508/#_Toc478383494
cheers
After lots of pain and trouble I found a decent solution to this problem. Since a wsdl file is provided, you should visit this site: : https://www.wsdl-analyzer.com
you can input a wsdl file and view all operations of the soap service. once you find the desired operation you want to call, click on it, and it will show an example request in xml. Some how, you have to generate this xml to make the request. There are many methods to do so, and some are more complicated than others. I found that manual serialization works well, and is honestly easier than using libraries.
say you have an operation request like this:
<s11:Envelope>
<s11:body>
<s11:operation>
<ns:username>username</ns:username>
<ns:password>password</ns:password>
</sll:operation>
</s11:body>
<s11:Envelope>
then you would generate by
public String gePayload(String username, String password) {
StringBuilder payload = new Stringbuilder();
payload.append("<s11:Envelope><s11:body><s11:operation>");
payload.append("<ns:username>");
payload.append(username);
payload.append("</ns:username>");
payload.append("<ns:password>");
payload.append(password);
payload.append("</ns:password>");
payload.append("</s11:operation></s11:body></s11:Envelope>");
return payload.toString()
}
then the web calls
public String callSoap(string payload) {
Webclient webclient = Webclient.builder()
// make sure the path is absolute
.baseUrl(yourEndPoint)
.build()
return WebClient.post()
.contentType(MediaType.TEXT_XML)
.bodyValue(payload)
.retrieve()
.bodyToMono(String.class)
.block();
}
it is important that you specify the content type is xml, and that the class returns a string. web flux cannot easily convert xml to user defined classes. so you do have to preform manual parsing. You can specify jaxb2xmlEncoders and jaxb2xmlDecoders to endcode/decode a specific class, but I found this to be to complicated. the payload has to match the request format generated by wsdl analyzer, and getting the encoders/decoders to match that format can be a task of its own. you can further research these encoders if you want, but this method will work.
I'm facing the same problem for a week and still can't find the best solution.
If you want to test the WebClient you just need to post a string with the SOAP Envelope request. Something like that:
String _request = "<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">\n" +
"<soap:Body>\n" +
"<request>\n" +
"<Example>blabla</Example>\n" +
"</request>\n" +
"</soap:Body>\n" +
"</soap:Envelope>";
WebClient webClient = WebClient.builder().baseUrl("http://example-service").build();
Mono<String> stringMono = webClient.post()
.uri("/example-port")
.body(BodyInserters.fromObject(_request))
.retrieve()
.bodyToMono(String.class);
stringMono.subscribe(System.out::println);
The problem is that you need to figure out how to serialize the whole SOAP Envelope (request and response) to a string.
This is only an example - not a solution.

AWS API Gateway: Request signature we calculated does not match the signature you provided

When I tried to hit the request through postman. I am facing the some problem. I set this in the aws authentication in postman:-
"Access key:- xxxxxxxxxx
Secrete key:-xxxxxxxxxx
Aws region:- ap-south-1
Service name:- execute-api".
while POST i got this error
{ "message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'POST\n/dms/api/v1/filestore/file\n\ncontent-length:29940\ncontent-type:multipart/form-data\nhost:e7ffona9b4.execute-api.ap-south-1.amazonaws.com\nx-amz-date:20170714T111258Z\n\ncontent-length;content-type;host;x-amz-date\n15c77e9022d5c4e9de523e7279515d245695c76115ebabe7517119701f9ae963'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\n20170714T111258Z\n20170714/ap-south-1/execute-api/aws4_request\nb0e6151f24eb3a4aa5520da442bdafe7625a44ed7c15f3109eeb78eef038a6cd'\n" }
while GET i got this
{ "message": "No method found matching route api/v1/filestore/file for http method GET." }
In the code I did something like this
#RestController
#requestmapping(value = "/v1/filestore")
public class FileUploadController {
#Autowired
private IFileStoreService fileStoreService;
#CrossOrigin
#ResponseBody
#RequestMapping(value = "/file/{id}", method = RequestMethod.GET)
and in application.properties i set
server.contextPath=/api
If I was wrong then please lead me the correct argument and help me to fix this issue.
Thanks
Double check these:
The key you're assigning to the object doesn't start with a period.
There should not be any invisible character while paste.
Try to encoding the CopySource with encodeURIComponent()
Debugging signature problems can be difficult and there could be multiple problems involved. Looking over your description, here's one problem I noticed...
In the error message that you're getting back using postman, we have:
The request signature we calculated does not match the signature...The Canonical String for this request should have been...content-type:multipart/form-data...
API Gateway does not support nor correctly handle multipart/form-data

Where "user" comes from in convertAndSendToUser works in SockJS+Spring Websocket?

I would like to understand how convertAndSendToUser works in Spring SockJS+Websocket framework.
In client, we would connect as
stompClient.connect(login, password, callback())
which will result in connect request with "Stomp credentials" of login and password, that can be seen e.g. if we handle SessionConnectEvent http://www.sergialmar.com/2014/03/detect-websocket-connects-and-disconnects-in-spring-4/
But it remains unclear to me whether this will be the "user" meant in server-side send operation to a queue:
simpMessagingTemplate.convertAndSendToUser(username, "/queue/reply", message);
The closest I can get is to read this thread Sending message to specific user on Spring Websocket, answer by Thanh Nguyen Van, but it is still unclear.
Basically what I need to do, is to subscribe some clients to same topic, but on server, send them different data. Client may supply user identifier.
We know we can send messages to the client from a stomp server using the topic prefixes that he is subscribed to e.g. /topic/hello. We also know we can send messages to a specific user because spring provides the convertAndSendToUser(username, destination, message) API. It accepts a String username which means if we somehow have a unique username for every connection, we should be able to send messages to specific users subscribed to a topic.
What's less understood is, where does this username come from ?
This username is part of a java.security.Principal interface. Each StompHeaderAccessor or WebSocketSession object has instance of this principal and you can get the user name from it. However, as per my experiments, it is not generated automatically. It has to be generated manually by the server for every session.
To use this interface first you need to implement it.
class StompPrincipal implements Principal {
String name
StompPrincipal(String name) {
this.name = name
}
#Override
String getName() {
return name
}
}
Then you can generate a unique StompPrincipal for every connection by overriding the DefaultHandshakeHandler. You can use any logic to generate the username. Here is one potential logic which uses UUID :
class CustomHandshakeHandler extends DefaultHandshakeHandler {
// Custom class for storing principal
#Override
protected Principal determineUser(
ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes
) {
// Generate principal with UUID as name
return new StompPrincipal(UUID.randomUUID().toString())
}
}
Lastly, you need to configure your websockets to use your custom handshake handler.
#Override
void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry
.addEndpoint("/stomp") // Set websocket endpoint to connect to
.setHandshakeHandler(new CustomHandshakeHandler()) // Set custom handshake handler
.withSockJS() // Add Sock JS support
}
That's It. Now your server is configured to generate a unique principal name for every connection. It will pass that principal as part of StompHeaderAccessor objects that you can access through connection event listeners, MessageMapping functions etc...
From event listeners :
#EventListener
void handleSessionConnectedEvent(SessionConnectedEvent event) {
// Get Accessor
StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage())
}
From Message Mapped APIs
#MessageMapping('/hello')
protected void hello(SimpMessageHeaderAccessor sha, Map message) {
// sha available in params
}
One last note about using convertAndSendToUser(...). When sending messages to a user, you will use something like this
convertAndSendToUser(sha.session.principal.name, '/topic/hello', message)
However, for subscribing the client, you will use
client.subscribe('/user/topic/hello', callback)
If you subscribe the client to /topic/hello you will only receive broadcasted messages.
I did not do any specific configuration and I can just do this:
#MessageMapping('/hello')
protected void hello(Principal principal, Map message) {
String username = principal.getName();
}
Similar to Wenneguen I was able to do just by injecting Principal in the MessageMapping method
public void processMessageFromClient(#Payload String message, Principal principal) {
the principal.getName() implementation is from org.springframework.security.authentication.UsernamePasswordAuthenticationToken
class

MessageMapping Handler - No matching methods

I have a Spring webservice #Controller class with a #MessageMapping annotated method as follows:
#MessageMapping("/trade")
public void executeTrade(MarketOrderRequest trade, Principal principal) {
trade.setUserID(principal.getName());
logger.debug("Trade: " + trade);
this.tradeService.executeTrade(trade);
}
I am sending a JSON string message built using the same MarketOrderRequest POJO as is accepted by the server method. With some Key:Value pairs which are set null (but are still present).
The WebSocketConfig class has configured the following endpoints:
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
When i try to send a message to this messagemapping using this code:
MarketOrderRequest request = new MarketOrderRequest();
//{set request variables..}
StompHeaders someHeaders = new StompHeaders();
someHeaders.putAll(sessionHeaders);
someHeaders.setDestination("/app/trade");
session.send(someHeaders, request);
With headers:
{Cookie=[JSESSIONID=8421F536B639126F84F12E655375D790; Path=/spring-websocket-portfolio/; HttpOnly], version=[1.2], heart-beat=[0,0], user-name=[fabrice], destination=[/app/trade]}
The server then prints that a method cannot be found for the request:
Searching methods to handle SEND /app/trade session=397da625042343b4bac1c913b6d8ec22 application/json;charset=UTF-8
payload={"uuid":null,"symbol":"EUR/USD","price":1.10182,"side":"1","qty":50000,"quoteID"...(truncated)
WebSocketAnnotationMethodMessageHandler[DEBUG] - No matching methods.
The server code is lifted from this project and altered slightly to suit my needs: link
I have added some role-based web socket security in an AbstractSecurityWebSocketMessageBrokerConfigurer implementation class as follows:
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated()
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
.simpDestMatchers("/app/**").hasAnyRole("roleA", "roleB", "roleC")
//{some more subscribe dest matchers by role which are working}
}
would this possibly effect the WebSocketAnnotationMethodMessageHandler's attempts to map the request? It is pretty much the only change I have made to the config. My subscribe mappings are working perfectly.
To me it seems that there is a problem finding the method due to either the JSON or Principal parameters. I am sending the correct object type so is this possibly a problem with the User principal? Thanks
There was an error in my WebSocketConfig class.
The #componentscan annotation had the wrong package name. I updated the name to the correct value ( the name of my base package eg "com.my.project" ). Now during deployment in the logs, I can see the controller resources being mapped to the methods in my class.
Eg log output for one method:
Mapped "{[/order],messageType=[MESSAGE]}" onto public void com.my.project.web.PortfolioController.executeOrder(tradeObjects.OrderRequest,java.security.Principal)

Categories

Resources