This is an automated email from the ASF dual-hosted git repository.

sergehuber pushed a commit to branch backport/graphql-websocket-auth-3.0.x
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit b9f6d6f0f0ec3663ff064a8dc7c4130e158587e3
Author: Serge Huber <[email protected]>
AuthorDate: Thu Aug 6 20:03:30 2026 +0200

    Require authentication for GraphQL WebSocket upgrade
    
    Backport of master fix adapted for 3.0.x JAAS-only GraphQL auth (no tenant 
API keys / SecurityService ThreadLocal on the socket).
---
 .../unomi/graphql/servlet/GraphQLServlet.java      |  3 ++
 .../auth/GraphQLServletSecurityValidator.java      | 19 ++++++++++
 .../unomi/itests/graphql/GraphQLWebSocketIT.java   | 44 +++++++++++++++++++++-
 3 files changed, 64 insertions(+), 2 deletions(-)

diff --git 
a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java
 
b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java
index c9bd2da58..15d082727 100644
--- 
a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java
+++ 
b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/GraphQLServlet.java
@@ -90,6 +90,9 @@ public class GraphQLServlet extends WebSocketServlet {
         LOGGER.debug("GraphQLServlet service called with request: {}", 
request.getRequestURI());
         if (factory.isUpgradeRequest(request, response)) {
             try {
+                if (!validator.validateWebSocketUpgrade(request, response)) {
+                    return;
+                }
                 final ServletUpgradeRequest upReq = new 
ServletUpgradeRequest(request);
                 for (String subProtocol : upReq.getSubProtocols()) {
                     if (subProtocol.startsWith("graphql")) {
diff --git 
a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java
 
b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java
index ca64228cd..5a2a6ea58 100644
--- 
a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java
+++ 
b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java
@@ -56,6 +56,25 @@ public class GraphQLServletSecurityValidator {
         parser = new Parser();
     }
 
+    /**
+     * Authenticates a WebSocket upgrade. Subscriptions are never public, so 
only Basic
+     * JAAS credentials are accepted.
+     *
+     * @return true when the caller is authenticated
+     */
+    public boolean validateWebSocketUpgrade(HttpServletRequest req, 
HttpServletResponse res) throws IOException {
+        if (req.getHeader("Authorization") == null) {
+            res.addHeader("WWW-Authenticate", "Basic realm=\"karaf\"");
+            res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+            return false;
+        }
+        if (isAuthenticatedUser(req)) {
+            return true;
+        }
+        res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+        return false;
+    }
+
     public boolean validate(String query, String operationName, 
HttpServletRequest req, HttpServletResponse res) throws IOException {
         if (isPublicOperation(query)) {
             return true;
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLWebSocketIT.java 
b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLWebSocketIT.java
index bcef7ebc0..a1cd1af1f 100644
--- 
a/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLWebSocketIT.java
+++ 
b/itests/src/test/java/org/apache/unomi/itests/graphql/GraphQLWebSocketIT.java
@@ -23,6 +23,7 @@ import io.reactivex.ObservableEmitter;
 import io.reactivex.subscribers.DefaultSubscriber;
 import org.eclipse.jetty.websocket.api.RemoteEndpoint;
 import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.UpgradeException;
 import org.eclipse.jetty.websocket.api.WebSocketAdapter;
 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
 import org.eclipse.jetty.websocket.client.WebSocketClient;
@@ -32,9 +33,12 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Base64;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 
@@ -52,6 +56,7 @@ public class GraphQLWebSocketIT extends BaseGraphQLIT {
 
             URI echoUri = new URI("ws://localhost:" + getHttpPort() + 
"/graphql");
             ClientUpgradeRequest request = new ClientUpgradeRequest();
+            request.setHeader("Authorization", 
basicAuthHeader(BASIC_AUTH_USER_NAME, BASIC_AUTH_PASSWORD));
 
             Future<Session> onConnected = client.connect(socket, echoUri, 
request);
             RemoteEndpoint remote = onConnected.get().getRemote();
@@ -74,8 +79,7 @@ public class GraphQLWebSocketIT extends BaseGraphQLIT {
 
             LOGGER.info("Waiting for socket to close...");
 
-            CloseStatus status = socket.waitClose().get(10, TimeUnit.SECONDS);
-            // Assert.assertEquals(1000, (int) status.getStatus()); TODO skip 
for now
+            socket.waitClose().get(10, TimeUnit.SECONDS);
 
         } finally {
             client.stop();
@@ -83,6 +87,42 @@ public class GraphQLWebSocketIT extends BaseGraphQLIT {
         }
     }
 
+    @Test
+    public void testWebSocketUpgrade_withoutAuth_returns401() throws Exception 
{
+        assertWebSocketUpgradeRejected(new ClientUpgradeRequest());
+    }
+
+    @Test
+    public void testWebSocketUpgrade_withWrongJaasPassword_returns401() throws 
Exception {
+        ClientUpgradeRequest request = new ClientUpgradeRequest();
+        request.setHeader("Authorization", 
basicAuthHeader(BASIC_AUTH_USER_NAME, "definitely-not-the-password"));
+        assertWebSocketUpgradeRejected(request);
+    }
+
+    private void assertWebSocketUpgradeRejected(ClientUpgradeRequest request) 
throws Exception {
+        WebSocketClient client = new WebSocketClient();
+        Socket socket = new Socket();
+        try {
+            client.start();
+            URI echoUri = new URI("ws://localhost:" + getHttpPort() + 
"/graphql");
+            Future<Session> onConnected = client.connect(socket, echoUri, 
request);
+            try {
+                onConnected.get(10, TimeUnit.SECONDS);
+                Assert.fail("Unauthenticated GraphQL WebSocket upgrade should 
be rejected");
+            } catch (ExecutionException e) {
+                Throwable cause = e.getCause();
+                Assert.assertTrue("Expected UpgradeException, got: " + cause, 
cause instanceof UpgradeException);
+                Assert.assertEquals(401, ((UpgradeException) 
cause).getResponseStatusCode());
+            }
+        } finally {
+            client.stop();
+        }
+    }
+
+    private static String basicAuthHeader(String user, String password) {
+        return "Basic " + Base64.getEncoder().encodeToString((user + ":" + 
password).getBytes(StandardCharsets.UTF_8));
+    }
+
     private class Socket extends WebSocketAdapter {
 
         private Flowable<String> publisher;

Reply via email to