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 93254f7c8e..bfc5d20f0e 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 @@ -84,6 +84,9 @@ public void configure(WebSocketServletFactory factory) { protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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 e3e36c8fff..d9d34cf0f1 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 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 bcef7ebc0e..128aca5469 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.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.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 void testWebSocketConnectionSegment() throws Exception { URI echoUri = new URI("ws://localhost:" + getHttpPort() + "/graphql"); ClientUpgradeRequest request = new ClientUpgradeRequest(); + request.setHeader("Authorization", basicAuthHeader(BASIC_AUTH_USER_NAME, BASIC_AUTH_PASSWORD)); Future onConnected = client.connect(socket, echoUri, request); RemoteEndpoint remote = onConnected.get().getRemote(); @@ -74,8 +79,7 @@ public void testWebSocketConnectionSegment() throws Exception { 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,44 @@ public void testWebSocketConnectionSegment() throws Exception { } } + @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 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 publisher;