Copilot commented on code in PR #844:
URL: https://github.com/apache/unomi/pull/844#discussion_r3935504183


##########
graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java:
##########
@@ -109,13 +110,55 @@ private boolean isPublicOperation(String query) {
         return true;
     }
 
+    /**
+     * Authenticates a WebSocket upgrade that carries an {@code Authorization} 
header. Subscriptions are
+     * never public, so only Basic JAAS credentials are accepted.
+     *
+     * @return true when the caller is authenticated; false after a 401 has 
been sent
+     */
+    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);

Review Comment:
   A 401 response must include a `WWW-Authenticate` challenge, but invalid or 
malformed credentials reach this branch without one. Add the same Basic 
challenge used by the missing-header branch so upgrade clients receive a 
standards-compliant response.



##########
graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocket.java:
##########
@@ -82,9 +169,85 @@ public void onWebSocketText(String textMessage) {
         }
     }
 
+    /**
+     * Authenticates the socket from the {@code connection_init} payload, 
which is how a browser client
+     * presents credentials (it cannot set request headers on the handshake). 
A socket that already
+     * authenticated on the upgrade is left as it is.
+     *
+     * @return true when the socket may proceed, false when it has been closed
+     */
+    private boolean handleConnectionInit(GraphQLMessage message) {
+        if (authenticated) {
+            return true;
+        }
+        if (isDeadlineExpired()) {
+            return refuseAfterDeadline(message);
+        }
+
+        final String credential = basicCredentialFrom(message.getPayload());
+        if (credential == null || validator == null || 
!validator.authenticateBasicCredential(credential)) {
+            LOGGER.warn("Refusing GraphQL WebSocket connection_init without a 
valid credential");
+            sendMessage(GraphQLMessage.create(message.getId())
+                    .type(GraphQLMessage.TYPE_CONNECTION_ERROR)
+                    .errors(Collections.singletonList("Not authenticated"))
+                    .build());
+            closeConnection(message, CLOSE_POLICY_VIOLATION, "Not 
authenticated");
+            return false;
+        }
+
+        synchronized (authenticationLock) {
+            // The deadline may have fired while the credential was being 
checked.
+            if (deadlineExpired) {
+                return refuseAfterDeadline(message);
+            }
+            this.authenticated = true;
+        }
+        cancelAuthenticationDeadline();
+        final Session session = getSession();
+        if (session != null) {
+            // Authenticated: drop the short unauthenticated deadline.
+            session.setIdleTimeout(0);

Review Comment:
   After browser-side `connection_init` authentication, Jetty interprets an 
idle timeout of `0` as disabled. This makes these sockets bypass the factory's 
normal configured idle limit indefinitely, unlike sockets authenticated during 
the upgrade. The scheduled task already enforces the authentication deadline, 
so preserve and restore the session's original timeout (or avoid changing it at 
all).



##########
graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidator.java:
##########
@@ -109,13 +110,55 @@ private boolean isPublicOperation(String query) {
         return true;
     }
 
+    /**
+     * Authenticates a WebSocket upgrade that carries an {@code Authorization} 
header. Subscriptions are
+     * never public, so only Basic JAAS credentials are accepted.
+     *
+     * @return true when the caller is authenticated; false after a 401 has 
been sent
+     */
+    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;
+    }
+
+    /**
+     * Authenticates a Basic credential that did not arrive on an HTTP 
request, such as the one a browser
+     * client sends in the WebSocket {@code connection_init} payload. Same 
JAAS check as the HTTP path.
+     */
+    public boolean authenticateBasicCredential(String authorizationValue) {
+        return authenticateBasic(authorizationValue, null);
+    }
+
     private boolean isAuthenticatedUser(HttpServletRequest req) {
         req.setAttribute(AUTHENTICATION_TYPE, HttpServletRequest.BASIC_AUTH);
+        return authenticateBasic(req.getHeader("Authorization"), req);
+    }
 
-        String authHeader = req.getHeader("Authorization");
-
-        String usernameAndPassword = new 
String(Base64.getDecoder().decode(authHeader.substring(6).getBytes()));
+    /**
+     * @param req the originating request, or {@code null} when the credential 
did not arrive on one
+     */
+    private boolean authenticateBasic(String authHeader, HttpServletRequest 
req) {
+        if (authHeader == null || !authHeader.startsWith("Basic ")) {

Review Comment:
   HTTP authentication scheme names are case-insensitive, so this exact-case 
check rejects valid headers such as `basic <credentials>` or `BASIC 
<credentials>` on both HTTP and WebSocket paths. Use a case-insensitive prefix 
comparison while retaining the existing credential decoding.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to