superset icon indicating copy to clipboard operation
superset copied to clipboard

Superset Async Query Bug - Websocket Connection Failed JWT not present

Open Haoran-Qi opened this issue 1 year ago • 6 comments

Bug description

Hi

I find a bug in the Async Query Websocket code while I'm trying to enable the Async query "ws" mode I followed the instructions and docs to set up all the config values https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries https://github.com/apache/superset/issues/9190

In the websocket code, it is looking for the channel_id from cookie encrypted by JWT https://github.com/apache/superset/blob/9900f566700e6d0a2c376aa4260a0a3f4db27404/superset-websocket/src/index.ts#L278

However in the sueprset_app's ws connection, we are not provbiding proper JWT token to establish WS connection https://github.com/apache/superset/blob/9900f566700e6d0a2c376aa4260a0a3f4db27404/superset-frontend/src/middleware/asyncEvent.ts#L196

Which lead to a JWT not present error.

I try to connects to the Websocket conatainer locally with following code, and it works

const jwt = require('jsonwebtoken');
const WebSocket = require('ws');


// Establish WebSocket connection
const jwts = "*******"
const token = jwt.sign(
    { "channel": "1234122341234" },
    jwts,
)
const socket = new WebSocket('ws://127.0.0.1:8080/', {
    headers: {
        cookie: "async-token=" + token
    }
});

// Event listener for when the connection is established
socket.onopen = function (event) {
    console.log('WebSocket connection established.');
};

I notice the we create the channel_id and set the cookie in async_mananger, but I don't think it reflects on websocket container https://github.com/apache/superset/blob/9900f566700e6d0a2c376aa4260a0a3f4db27404/superset/async_events/async_query_manager.py#L157

How to reproduce the bug

  1. follow the instruction to enable Async query
  2. docker compose up
  3. you can see the superset_app cannot connect to websocket container because of the JWT token missing

image

my config GLOBAL_ASYNC_QUERIES_JWT_SECRET = "***********" GLOBAL_ASYNC_QUERIES_TRANSPORT = "ws" GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE = True GLOBAL_ASYNC_QUERIES_REDIS_CONFIG = { "port": REDIS_PORT, "host": REDIS_HOST, "password": "", "db": 0, "ssl": False, }

Screenshots/recordings

No response

Superset version

master / latest-dev

Python version

3.9

Node version

16

Browser

Chrome

Additional context

No response

Checklist

  • [X] I have searched Superset docs and Slack and didn't find a solution to my problem.
  • [X] I have searched the GitHub issue tracker and didn't find a similar bug report.
  • [X] I have checked Superset's logs for errors and if I found a relevant Python stacktrace, I included it here as text in the "additional context" section.

Haoran-Qi avatar Feb 28 '24 21:02 Haoran-Qi

Did you make a copy of superset-websocket/config.example.json as superset-websocket/config.json and change the jwtSecret value? You can use https://jwt.io/ to generate a valid token. I don't think it matters what the payload is, just have to be a valid JWT.

ericho-bbai avatar Feb 28 '24 23:02 ericho-bbai

Yeah, I did make the superset-websocket/config.json file with 32 bytes secret key, same key as the GLOBAL_ASYNC_QUERIES_JWT_SECRET I set up in docker/pythonpath_dev/superset_config.py.

In superset app container https://github.com/apache/superset/blob/9900f566700e6d0a2c376aa4260a0a3f4db27404/superset-frontend/src/middleware/asyncEvent.ts#L196

I don't think it provids the JWT in the header, that's why websocket container is giving a JWT not present error

If the token is invalid, it should throw an error with the jwt verification failure in the following line.

https://github.com/apache/superset/blob/9900f566700e6d0a2c376aa4260a0a3f4db27404/superset-websocket/src/index.ts#L282

Haoran-Qi avatar Feb 29 '24 15:02 Haoran-Qi

I am facing the same issue with Kubernetes deployment. JWT

amitdixit avatar Mar 04 '24 14:03 amitdixit

Is anyone still facing this? It seems to have gone silent, so may eventually be closed as stale. In the meantime, the websocket connection is being addressed in a forthcoming SIP by @villebro and subject to architecture redesign.

For good measure, I'll also ping @dosu-bot in case it's helpful here.

rusackas avatar Jul 30 '24 23:07 rusackas

Hey @rusackas! I'm here to help you with any bugs, questions, or contributions. I'm Dosu, a bot designed to assist you while you wait for a human maintainer. Let me know how I can help!

To address the "JWT not present" error in the WebSocket connection for Superset's async query feature, ensure the following configurations and steps are correctly implemented:

  1. Configuration Settings:

    • Ensure that the GLOBAL_ASYNC_QUERIES_JWT_SECRET and GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME are set correctly in your superset_config.py and match the values in superset-websocket/config.json.
  2. Environment Variables:

    • Set the JWT_SECRET and JWT_COOKIE_NAME environment variables to match the values used in your configuration.
  3. WebSocket Configuration:

    • Ensure the superset-websocket/config.json file is correctly configured with the jwtSecret and jwtCookieName values. Here is an example configuration:
      {
        "port": 8080,
        "logLevel": "info",
        "logToFile": false,
        "logFilename": "app.log",
        "statsd": {
          "host": "127.0.0.1",
          "port": 8125,
          "globalTags": []
        },
        "redis": {
          "port": 6379,
          "host": "127.0.0.1",
          "password": "",
          "db": 0,
          "ssl": false
        },
        "redisStreamPrefix": "async-events-",
        "jwtAlgorithms": ["HS256"],
        "jwtSecret": "YOUR_32_BYTE_SECRET_KEY",
        "jwtCookieName": "async-token"
      }
      
  4. Session and Cookie Handling:

    • The async_query_manager.py file should correctly create the channel_id and set the JWT cookie. Ensure the validate_session method is properly setting the cookie:
      def register_request_handlers(self, app: Flask) -> None:
          @app.after_request
          def validate_session(response: Response) -> Response:
              user_id = get_user_id()
              reset_token = (
                  not request.cookies.get(self._jwt_cookie_name)
                  or "async_channel_id" not in session
                  or "async_user_id" not in session
                  or user_id != session["async_user_id"]
              )
              if reset_token:
                  async_channel_id = str(uuid.uuid4())
                  session["async_channel_id"] = async_channel_id
                  session["async_user_id"] = user_id
                  sub = str(user_id) if user_id else None
                  token = jwt.encode(
                      {"channel": async_channel_id, "sub": sub},
                      self._jwt_secret,
                      algorithm="HS256",
                  )
                  response.set_cookie(
                      self._jwt_cookie_name,
                      value=token,
                      httponly=True,
                      secure=self._jwt_cookie_secure,
                      domain=self._jwt_cookie_domain,
                      samesite=self._jwt_cookie_samesite,
                  )
              return response
      
  5. WebSocket Connection:

    • Ensure the WebSocket connection is established with the correct JWT token in the headers. Here is an example of how to manually create a JWT token and establish a WebSocket connection:
      const jwt = require('jsonwebtoken');
      const WebSocket = require('ws');
      const token = jwt.sign(
          { "channel": "1234122341234" },
          "YOUR_32_BYTE_SECRET_KEY"
      );
      const socket = new WebSocket('ws://127.0.0.1:8080/', {
          headers: {
              cookie: "async-token=" + token
          }
      });
      socket.onopen = function (event) {
          console.log('WebSocket connection established.');
      };
      
  6. Check for Updates:

    • There have been updates related to WebSocket connections and JWT tokens in Superset. Refer to the pull request #25891 for changes that support different JWT CSRF cookie names, which might address related issues.

By ensuring these configurations and steps are correctly implemented, the "JWT not present" error should be resolved [1][2][3].

To continue talking to Dosu, mention @dosu.

dosubot[bot] avatar Jul 30 '24 23:07 dosubot[bot]

I'm still having the same problem.

ptdtan avatar Oct 15 '24 06:10 ptdtan

I encountered the same problem. When using the Docker Compose UP to build an superset environment, websocket will report to the error of WebSocket Connection Failed JWT not present. {"level":"error","message":"JWT not present","stack":"Error: JWT not present\n at readChannelId (/home/superset-websocket/dist/index.js:242:15)\n at Server.httpUpgrade (/home/superset-websocket/dist/index.js:337:9)\n at Server.emit (node:events:513:28)\n at onParserExecuteCommon (node:_http_server:772:14)\n at onParserExecute (node:_http_server:686:3)"}

Debug found that JWT Token was not stored in Cookies.

https://github.com/apache/superset/blob/9900f566700e6d0a2c376aa4260a0a3f4db27404/superset-websocket/src/index.ts#L279

FullStackHan avatar Jan 09 '25 07:01 FullStackHan

bumping because I am also having this issue on 4.0.2

thomasrea0113 avatar Mar 31 '25 20:03 thomasrea0113

To resolve this issue, you need to:

  1. Update the FEATURE_FLAGS in your superset_config.py file
  2. Specifically, set GLOBAL_ASYNC_QUERIES to True to override the default value in superset/config.py

This setting is necessary for the token to be set in your cookies - without this change, the tokens will not be set at all

You could check whether this flag is set printing this to console in your browser in your superset app: console.log(window.featureFlags)

Additionally, please review all the points mentioned by @dosubot in the earlier comments, they are still valid, except JWT_SECRET & JWT_COOKIE_NAME

drparavozik avatar May 15 '25 14:05 drparavozik