apache/superset · critical · AsyncQueryTokenException

Please provide a JWT secret at least 32 bytes long

Error message

Please provide a JWT secret at least 32 bytes long

What it means

AsyncQueryTokenException raised during AsyncQueryManager.init_app() when GLOBAL_ASYNC_QUERIES_JWT_SECRET is shorter than 32 bytes. The JWT secret signs the per-user channel tokens stored in a cookie that authorize access to the Redis event stream, so a short secret would be brute-forceable; Superset enforces a minimum length at startup.

Source

Thrown at superset/async_events/async_query_manager.py:144

        self._load_chart_data_into_cache_job: Any = None
        # pylint: disable=invalid-name

    def init_app(self, app: Flask) -> None:
        cache_type = app.config.get("CACHE_CONFIG", {}).get("CACHE_TYPE")
        data_cache_type = app.config.get("DATA_CACHE_CONFIG", {}).get("CACHE_TYPE")
        if cache_type in [None, "null"] or data_cache_type in [None, "null"]:
            raise Exception(  # pylint: disable=broad-exception-raised
                """
                Cache backends (CACHE_CONFIG, DATA_CACHE_CONFIG) must be configured
                and non-null in order to enable async queries
                """
            )

        self._cache = get_cache_backend(app.config)
        logger.debug("Using GAQ Cache backend as %s", type(self._cache).__name__)

        if len(app.config["GLOBAL_ASYNC_QUERIES_JWT_SECRET"]) < 32:
            raise AsyncQueryTokenException(
                "Please provide a JWT secret at least 32 bytes long"
            )

        self._stream_prefix = app.config["GLOBAL_ASYNC_QUERIES_REDIS_STREAM_PREFIX"]
        self._stream_limit = app.config["GLOBAL_ASYNC_QUERIES_REDIS_STREAM_LIMIT"]
        self._stream_limit_firehose = app.config[
            "GLOBAL_ASYNC_QUERIES_REDIS_STREAM_LIMIT_FIREHOSE"
        ]
        self._jwt_cookie_name = app.config["GLOBAL_ASYNC_QUERIES_JWT_COOKIE_NAME"]
        self._jwt_cookie_secure = app.config["GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SECURE"]
        self._jwt_cookie_samesite = app.config[
            "GLOBAL_ASYNC_QUERIES_JWT_COOKIE_SAMESITE"
        ]
        self._jwt_cookie_domain = app.config["GLOBAL_ASYNC_QUERIES_JWT_COOKIE_DOMAIN"]
        self._jwt_secret = app.config["GLOBAL_ASYNC_QUERIES_JWT_SECRET"]
        self._jwt_expiration_seconds = app.config[
            "GLOBAL_ASYNC_QUERIES_JWT_EXPIRATION_SECONDS"
        ]

View on GitHub (pinned to f4587218dd)

Solutions

  1. Generate a proper secret: python -c "import secrets; print(secrets.token_urlsafe(64))" and set GLOBAL_ASYNC_QUERIES_JWT_SECRET to it (>= 32 chars).
  2. Store it in a secret manager / environment variable rather than hardcoding, then reference it in superset_config.py.
  3. Set the same secret on webserver and all Celery workers — tokens are signed and verified across processes.

Example fix

# before
GLOBAL_ASYNC_QUERIES_JWT_SECRET = "supersecret"

# after
import os
GLOBAL_ASYNC_QUERIES_JWT_SECRET = os.environ["SUPERSET_JWT_SECRET"]  # >= 32 chars, e.g. secrets.token_urlsafe(64)
Defensive patterns

Strategy: validation

Validate before calling

import os, secrets
secret = os.environ.get("GLOBAL_ASYNC_QUERIES_JWT_SECRET", "")
if len(secret.encode()) < 32:
    secret = secrets.token_urlsafe(64)
    # persist via your secret manager; never fall back silently in production
assert len(secret.encode()) >= 32

Prevention

When it happens

Trigger: Enabling GLOBAL_ASYNC_QUERIES with GLOBAL_ASYNC_QUERIES_JWT_SECRET set to a short string (e.g. 'secret' or a 16-char value), or leaving it unset so an empty/default value is checked. Raised at init_app time, blocking app and worker startup.

Common situations: Copy-pasting example config with a placeholder secret; generating a secret with too few characters; upgrading deployments where the secret was previously optional; CI configs that reuse a short dummy secret.

Related errors


AI-assisted analysis of apache/superset@f4587218dd (2026-08-14). Data as JSON: /api/errors/f8500bac8df09727. Report an issue: GitHub.