This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch fix/graphql-websocket-auth
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit 5ca094dde7c95dad8fe4ace34b85c24eb40e78e3
Author: Serge Huber <[email protected]>
AuthorDate: Thu Sep 3 19:39:30 2026 +0200

    Authenticate the GraphQL playground's subscription WebSocket
    
    The shipped GraphiQL playground opened its subscription WebSocket with only 
a
    URL, so it presented no credential over a handshake that authenticates from 
the
    connection_init payload, leaving subscriptions unusable from the browser.
    
    The browser WebSocket API cannot set request headers on the upgrade, so 
reuse
    the Authorization the operator enters in GraphiQL's Headers tab instead: 
GraphiQL
    hands that editor's live content to the fetcher on every request, so 
capture it
    there and return it as the graphql-ws connection parameters, which are sent 
in
    the connection_init payload. HTTP and WebSocket then authenticate with the 
same
    credential, and it stays in memory rather than being written to browser 
storage.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../src/main/resources/assets/js/index.jsx         | 30 ++++++++++++++++++++--
 1 file changed, 28 insertions(+), 2 deletions(-)

diff --git a/graphql/graphql-ui/src/main/resources/assets/js/index.jsx 
b/graphql/graphql-ui/src/main/resources/assets/js/index.jsx
index a0869796f..a1de4467e 100644
--- a/graphql/graphql-ui/src/main/resources/assets/js/index.jsx
+++ b/graphql/graphql-ui/src/main/resources/assets/js/index.jsx
@@ -33,11 +33,37 @@ function graphqlWsUrl() {
     return protocol + '//' + window.location.host + '/graphql';
 }
 
+// The browser WebSocket API cannot set request headers on the handshake, so 
the server authenticates a
+// subscription from the connection_init payload instead. GraphiQL hands the 
live "Headers" tab content
+// to the fetcher on every request, so capture it here and reuse its 
Authorization as the WebSocket
+// connection parameters: HTTP and WebSocket then use the same credential, and 
nothing is persisted.
+let latestHeaders = null;
+
+function authorizationHeader() {
+    if (!latestHeaders) {
+        return null;
+    }
+    const key = Object.keys(latestHeaders).find((name) => name.toLowerCase() 
=== 'authorization');
+    return key && latestHeaders[key] ? latestHeaders[key] : null;
+}
+
 function createFetcher() {
-    return createGraphiQLFetcher({
+    const fetcher = createGraphiQLFetcher({
         url: graphqlHttpUrl(),
-        wsClient: createClient({ url: graphqlWsUrl() }),
+        wsClient: createClient({
+            url: graphqlWsUrl(),
+            // Evaluated on each (re)connect, and sent as the connection_init 
payload.
+            connectionParams: () => {
+                const authorization = authorizationHeader();
+                return authorization ? { Authorization: authorization } : {};
+            },
+        }),
     });
+
+    return (graphQLParams, opts) => {
+        latestHeaders = (opts && opts.headers) || null;
+        return fetcher(graphQLParams, opts);
+    };
 }
 
 function QueryPlayground() {

Reply via email to