You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2026/09/21 19:46:31 UTC
(unomi) 01/02: Challenge on every WebSocket 401, case-insensitive scheme, restore idle timeout
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch graphql-ws-auth-review-followups
in repository https://gitbox.apache.org/repos/asf/unomi.git
commit ee858e7f1e421b0ea6eaf306f3aca8dbe8237094
Author: Serge Huber <sh...@apache.org>
AuthorDate: Fri Sep 4 18:23:52 2026 +0200
Challenge on every WebSocket 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.
Co-Authored-By: Claude Opus 4.8 <no...@anthropic.com>
---
.../servlet/auth/GraphQLServletSecurityValidator.java | 5 ++++-
.../graphql/servlet/websocket/SubscriptionWebSocket.java | 8 ++++++--
.../servlet/auth/GraphQLServletSecurityValidatorTest.java | 15 +++++++++++++++
.../servlet/websocket/SubscriptionWebSocketTest.java | 4 +++-
4 files changed, 28 insertions(+), 4 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 199f6394a..97624d7f1 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
@@ -87,6 +87,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;
}
@@ -290,7 +292,8 @@ public class GraphQLServletSecurityValidator {
* (WebSocket {@code connection_init}); when null, no tenant header is consulted.
*/
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;
}
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 71299eb19..cf0a0c852 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
@@ -69,6 +69,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 final SecurityService securityService;
@@ -101,6 +104,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: Jetty resets it on any frame, including ping/pong control frames that never reach
// onWebSocketText, so a client could hold an unauthenticated socket open just by pinging.
@@ -265,8 +269,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/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidatorTest.java b/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidatorTest.java
index 8a664a98f..58c4b450b 100644
--- a/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidatorTest.java
+++ b/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/auth/GraphQLServletSecurityValidatorTest.java
@@ -240,6 +240,19 @@ class GraphQLServletSecurityValidatorTest {
verify(response, never()).sendError(any(Integer.class));
}
+ /** HTTP authentication scheme names are case-insensitive. */
+ @Test
+ void validateWebSocketUpgrade_withLowercaseScheme_isAccepted() throws IOException {
+ when(request.getHeader("Authorization")).thenReturn(BASIC_AUTH.replaceFirst("Basic", "basic"));
+ when(tenantService.getTenantByApiKey(any(), eq(ApiKey.ApiKeyType.PRIVATE))).thenReturn(null);
+
+ boolean authenticated = validator.validateWebSocketUpgrade(request, response);
+
+ assertTrue(authenticated);
+ verify(securityService).setCurrentSubject(any(Subject.class));
+ verify(response, never()).sendError(any(Integer.class));
+ }
+
@Test
void validateWebSocketUpgrade_rejectsPublicApiKeyOnly() throws IOException {
// No Authorization header — public API key alone must not open subscriptions.
@@ -261,6 +274,8 @@ class GraphQLServletSecurityValidatorTest {
assertFalse(authenticated);
verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
+ // A 401 must carry a challenge whichever branch produced it.
+ verify(response).addHeader("WWW-Authenticate", "Basic realm=\"karaf\"");
verify(securityService, never()).setCurrentSubject(any());
}
diff --git a/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocketTest.java b/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocketTest.java
index c874f2a7a..895d422c2 100644
--- a/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocketTest.java
+++ b/graphql/cxs-impl/src/test/java/org/apache/unomi/graphql/servlet/websocket/SubscriptionWebSocketTest.java
@@ -151,6 +151,7 @@ class SubscriptionWebSocketTest {
when(validator.authenticateBasicCredential(anyString())).thenReturn(true);
when(securityService.getCurrentSubject()).thenReturn(subject);
when(executionContextManager.getCurrentContext()).thenReturn(executionContext);
+ when(session.getIdleTimeout()).thenReturn(30_000L);
SubscriptionWebSocket unauth = new SubscriptionWebSocket(graphQL, serviceManager, null, null,
securityService, executionContextManager, validator, deadlineScheduler);
unauth.onWebSocketConnect(session);
@@ -161,7 +162,8 @@ class SubscriptionWebSocketTest {
// Identity captured onto the socket, and not left bound to this shared IO thread.
verify(securityService).clearCurrentSubject();
verify(session, never()).close(anyInt(), anyString());
- verify(session).setIdleTimeout(0);
+ // Back to the session's configured idle timeout, not disabled.
+ verify(session).setIdleTimeout(30_000L);
}
@Test