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;
