This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch
backport/graphql-websocket-auth-3.0.x
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to
refs/heads/backport/graphql-websocket-auth-3.0.x by this push:
new bcd07db2e Address review: challenge on every 401, case-insensitive
scheme, restore idle timeout
bcd07db2e is described below
commit bcd07db2e81f082b3ecd88adedd62edc0ece4a88
Author: Serge Huber <[email protected]>
AuthorDate: Fri Sep 4 18:21:49 2026 +0200
Address review: challenge on every 401, case-insensitive scheme, restore
idle timeout
A WebSocket upgrade whose credential is refused now receives the same
WWW-Authenticate challenge as one that carries no credential, as a 401 must.
The Basic scheme token is compared case-insensitively, as HTTP
authentication
schemes are. A socket that authenticates through connection_init now goes
back
to the idle timeout its session was configured with instead of having the
timeout disabled, so both authentication paths share the same idle
behaviour.
Adds an integration test authenticating with a lowercase scheme on the
handshake.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
.../auth/GraphQLServletSecurityValidator.java | 5 ++++-
.../servlet/websocket/SubscriptionWebSocket.java | 8 ++++++--
.../unomi/itests/graphql/GraphQLWebSocketIT.java | 20 ++++++++++++++++++++
3 files changed, 30 insertions(+), 3 deletions(-)
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 ea0addd95..717bf4cb7 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
@@ -125,6 +125,8 @@ public class GraphQLServletSecurityValidator {
if (isAuthenticatedUser(req)) {
return true;
}
+ // A 401 carries a challenge whether the header was missing or its
credential was refused.
+ res.addHeader("WWW-Authenticate", "Basic realm=\"karaf\"");
res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
@@ -146,7 +148,8 @@ public class GraphQLServletSecurityValidator {
* @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 ")) {
+ // The scheme token is case-insensitive (RFC 7235).
+ if (authHeader == null || !authHeader.regionMatches(true, 0, "Basic ",
0, 6)) {
return false;
}
final String usernameAndPassword;
diff --git
a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocket.java
b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocket.java
index 165d7ca9e..84697cb1d 100644
---
a/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocket.java
+++
b/graphql/cxs-impl/src/main/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocket.java
@@ -65,6 +65,9 @@ public class SubscriptionWebSocket extends WebSocketAdapter {
private volatile ScheduledFuture<?> deadlineTask;
+ /** The session's configured idle timeout, shortened while unauthenticated
and restored on authentication. */
+ private volatile long configuredIdleTimeout;
+
private boolean deadlineExpired;
private Map<String, ExecutionResultSubscriber> subscriptions = new
HashMap<String, ExecutionResultSubscriber>();
@@ -88,6 +91,7 @@ public class SubscriptionWebSocket extends WebSocketAdapter {
LOGGER.info("Opening web socket");
super.onWebSocketConnect(sess);
if (!authenticated) {
+ configuredIdleTimeout = sess.getIdleTimeout();
// Bound how long an unauthenticated socket may sit open. The idle
timeout alone is not a
// deadline, since Jetty resets it on any received frame, so a
scheduled task closes the
// socket at the deadline whatever the client sends.
@@ -205,8 +209,8 @@ public class SubscriptionWebSocket extends WebSocketAdapter
{
cancelAuthenticationDeadline();
final Session session = getSession();
if (session != null) {
- // Authenticated: drop the short unauthenticated deadline.
- session.setIdleTimeout(0);
+ // Authenticated: back to the idle timeout the session was
configured with, not "none".
+ session.setIdleTimeout(configuredIdleTimeout);
}
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 18c4e9c6c..a08e13c10 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
@@ -183,6 +183,26 @@ public class GraphQLWebSocketIT extends BaseGraphQLIT {
}
}
+ /** The HTTP authentication scheme token is case-insensitive. */
+ @Test
+ public void testWebSocketUpgrade_withLowercaseBasicScheme_succeeds()
throws Exception {
+ WebSocketClient client = new WebSocketClient();
+ Socket socket = new Socket();
+ try {
+ client.start();
+ ClientUpgradeRequest request = new ClientUpgradeRequest();
+ request.setHeader("Authorization", "basic " +
Base64.getEncoder().encodeToString(
+ (BASIC_AUTH_USER_NAME + ":" +
BASIC_AUTH_PASSWORD).getBytes(StandardCharsets.UTF_8)));
+ RemoteEndpoint remote = client.connect(socket,
graphqlWebSocketUri(), request).get(10, TimeUnit.SECONDS).getRemote();
+
remote.sendString(resourceAsString("graphql/socket/out/init.json"));
+
Assert.assertEquals(resourceAsString("graphql/socket/in/ack.json"),
socket.waitMessage().get(10, TimeUnit.SECONDS));
+
remote.sendString(resourceAsString("graphql/socket/out/term.json"));
+ Assert.assertEquals(1000, (int) socket.waitClose().get(10,
TimeUnit.SECONDS).getStatus());
+ } finally {
+ client.stop();
+ }
+ }
+
@Test
public void testWebSocketUpgrade_withWrongJaasPassword_returns401() throws
Exception {
assertWebSocketUpgradeRejected(basicAuthHeader(BASIC_AUTH_USER_NAME,
"definitely-not-the-password"), null, 401);