6

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?

  1. If I make the onOpen() return a Uni, how do I subscribe to it so it runs?
  2. Can I still access the JWT to get the claims I need?
  3. 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

Murrah
  • 1,508
  • 1
  • 13
  • 26
  • So still not sure what is the problem? In short 1-> Why do you want to make `onOpen()` return anything? 2-> Yes you can 3-> Yes it does – Babl Jan 20 '23 at 08:02
  • Thanks. I want to return a Uni to resolve the exception that is being thrown as mentioned above. ie it says: `Make the secured method return an async type, i.e. Uni, Multi or CompletionStage,`. It seems that is required in order for the async resolution of the authentication with Keycloak, which needs to happen in order to know whether the endpoint should be allowed or not, and to validate the access token. – Murrah Jan 20 '23 at 11:08
  • Hmm, that's strange, as that should not be a problem. Do you have a small POC where we can reproduce the problem? – Babl Jan 20 '23 at 14:39

0 Answers0