cube-js/cube · error · CubejsHandlerError

Unable to decode JWT key

Error message

Unable to decode JWT key

What it means

When Cube is configured to accept JWTs without public-key verification (decode-only checkAuth), it calls jwt.decode(auth, { complete: true }). If the Authorization token cannot be decoded as a JWT at all, the gateway responds 403 Forbidden with 'Unable to decode JWT key'. This means the token is not a syntactically valid JWT (header.payload.signature with base64 segments).

Source

Thrown at packages/cubejs-api-gateway/src/gateway.ts:2671

          this.logger('JWKs Background Fetching Error', {
            error: e.message,
          });
        },
      });

      this.releaseListeners.push(jwks.release);

      // Precache JWKs response to speedup first auth
      if (options.jwkUrl && typeof options.jwkUrl === 'string') {
        jwks.fetchOnly(options.jwkUrl).catch((e) => this.logger('JWKs Prefetching Error', {
          error: e.message,
        }));
      }

      checkAuthFn = async (auth) => {
        const decoded = <Record<string, any> | null>jwt.decode(auth, { complete: true });
        if (!decoded) {
          throw new CubejsHandlerError(
            403,
            'Forbidden',
            'Unable to decode JWT key'
          );
        }

        if (!decoded.header || !decoded.header.kid) {
          throw new CubejsHandlerError(
            403,
            'Forbidden',
            'JWT without kid inside headers'
          );
        }

        const jwk = await jwks.getJWKbyKid(
          typeof options.jwkUrl === 'function' ? await options.jwkUrl(decoded) : <string>options.jwkUrl,
          decoded.header.kid
        );

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Inspect the Authorization header and confirm it is a well-formed JWT (three dot-separated base64url segments); jwt.io can verify decode-ability.
  2. If your deployment uses shared secrets/tokens rather than JWTs, either issue a real JWT from your auth backend or change Cube's checkAuth configuration to match the token type.
  3. Fix token transport: send `Authorization: Bearer <jwt>` with no truncation, whitespace, or double-encoding; regenerate the token if it was corrupted.
  4. Verify the client SDK's token option receives the JWT (not an API key) — log the token before the request.

Example fix

// before
headers: { Authorization: 'my-secret-api-key' } // not a JWT, server does jwt.decode
// after
const token = jwt.sign({}, CUBEJS_SECRET, { expiresIn: '1d' });
headers: { Authorization: `Bearer ${token}` }
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeJwt(token) {
  return typeof token === 'string' && token.trim().split('.').length === 3 && token.split('.').every(p => p.length > 0);
}
if (!looksLikeJwt(token)) throw new Error('Authorization value is not a decodable JWT');

Type guard

function isJwt(v: unknown): v is string {
  return typeof v === 'string' && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$/.test(v.trim());
}

Try / catch

try {
  return await api.request();
} catch (e) {
  if (e?.status === 403 && String(e?.message).includes('Unable to decode JWT')) {
    const fresh = await fetchNewJwt();
    api.updateAuthorization(`Bearer ${fresh}`);
    return await api.request();
  }
  throw e;
}

Prevention

When it happens

Trigger: Request with an Authorization header whose value is not a decodable JWT: an opaque API token/secret pasted in place of a JWT, a token truncated or double-base64 encoded, an empty/whitespace token, or a session id.

Common situations: Conflicting auth setups — client sends a shared secret while the server expects a JWT (or vice versa after a config change); token generator producing raw base64 instead of proper JWT; tokens mangled by extra 'Bearer ' handling or whitespace/newlines; expired tokens that were stripped to an invalid fragment.

Understand the failure class

Related errors


AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02). Data as JSON: /api/errors/be4d57873f1985a3. Report an issue: GitHub.