apache / superset

Apache Superset is a Data Visualization and Data Exploration Platform
https://superset.apache.org/
Apache License 2.0
62.01k stars 13.59k forks source link

Superset Async Query Bug - Websocket Connection Failed JWT not present #27293

Open Haoran-Qi opened 7 months ago

Haoran-Qi commented 7 months ago

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

ericho-bbai commented 7 months ago

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.

Haoran-Qi commented 7 months ago

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

amitdixit commented 6 months ago

I am facing the same issue with Kubernetes deployment. JWT

rusackas commented 2 months ago

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.

dosubot[bot] commented 2 months ago

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.