From 6f4c1da8e262d60fb71dc291749d9257464b3794 Mon Sep 17 00:00:00 2001 From: "kolega.dev" Date: Mon, 9 Feb 2026 12:35:20 +0000 Subject: [PATCH] security: authenticate GraphQL subscription WebSocket connections The onConnect handler for GraphQL subscriptions was empty, allowing any client to establish a WebSocket connection and subscribe to loggingLiveTrail without authentication. Added JWT verification in onConnect using the same RS256 credentials and permission checks (manage:system) used elsewhere. --- client/client-app.js | 6 +++++- server/core/servers.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/client/client-app.js b/client/client-app.js index 0d2018d75..cdf27a804 100644 --- a/client/client-app.js +++ b/client/client-app.js @@ -114,7 +114,11 @@ const graphQLWSLink = new WebSocketLink({ uri: graphQLWSEndpoint, options: { reconnect: true, - lazy: true + lazy: true, + connectionParams: () => { + const token = Cookies.get('jwt') + return token ? { token } : {} + } } }) diff --git a/server/core/servers.js b/server/core/servers.js index 95046e657..d2b2b79f3 100644 --- a/server/core/servers.js +++ b/server/core/servers.js @@ -4,6 +4,8 @@ const https = require('https') const { ApolloServer } = require('apollo-server-express') const Promise = require('bluebird') const _ = require('lodash') +const jwt = require('jsonwebtoken') +const cookie = require('cookie') /* global WIKI */ @@ -125,7 +127,35 @@ module.exports = { context: ({ req, res }) => ({ req, res }), subscriptions: { onConnect: (connectionParams, webSocket) => { + let token = _.get(connectionParams, 'token', null) + if (!token) { + const cookieHeader = _.get(webSocket, 'upgradeReq.headers.cookie', '') + if (cookieHeader) { + const cookies = cookie.parse(cookieHeader) + token = cookies.jwt || null + } + } + + if (!token) { + throw new Error('Unauthorized') + } + + try { + const user = jwt.verify(token, WIKI.config.certs.public, { + audience: WIKI.config.auth.audience, + issuer: 'urn:wiki.js', + algorithms: ['RS256'] + }) + + if (!_.includes(user.permissions, 'manage:system')) { + throw new Error('Forbidden') + } + + return { user } + } catch (err) { + throw new Error('Unauthorized') + } }, path: '/graphql-subscriptions' }