cube-js/cube · error · CubejsHandlerError

JWT without kid inside headers

Error message

JWT without kid inside headers

What it means

When JWKS-based auth is configured (jwkUrl option), Cube decodes the incoming JWT and requires the token header to contain a `kid` (key ID) so it can select the matching signing key from the JWKS endpoint. If the token decodes but its header has no `kid`, the gateway throws this 403 Forbidden error because it cannot determine which key to verify against.

Source

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

      // 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
        );
        if (!jwk) {
          throw new CubejsHandlerError(
            403,
            'Forbidden',
            `Unable to verify, JWK with kid: "${decoded.header.kid}" not found`
          );
        }

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Configure your identity provider / token issuer to include the `kid` header in issued JWTs (e.g. in jsonwebtoken: jwt.sign(payload, key, { keyid: 'my-key-id' })).
  2. Ensure the JWKS endpoint publishes the key whose `kid` matches the one your tokens carry.
  3. If you don't use JWKS, remove the jwkUrl option and use a static `key` or a custom `checkAuth` function instead.
  4. Regenerate/reissue client tokens after fixing the issuer so old kid-less tokens are not in circulation.

Example fix

// before
const token = jwt.sign(payload, privateKey);
// after
const token = jwt.sign(payload, privateKey, { keyid: 'cube-key-1' });
Defensive patterns

Strategy: validation

Validate before calling

function hasKidHeader(token) {
  const decoded = jwt.decode(token, { complete: true });
  return Boolean(decoded && decoded.header && decoded.header.kid);
}
if (!hasKidHeader(myToken)) throw new Error('Token lacks kid header; reissue with keyid');

Type guard

function isDecodedJwtWithKid(v: unknown): v is { header: { kid: string; alg: string }, payload: object } {
  const d = v as any;
  return !!d && typeof d === 'object' && !!d.header && typeof d.header.kid === 'string' && d.header.kid.length > 0;
}

Try / catch

try {
  await cubeApi.load(query);
} catch (e) {
  if (e.status === 403 && /kid/i.test(e.message)) {
    // reissue token with keyid header before retrying
  }
  throw e;
}

Prevention

When it happens

Trigger: A request with an Authorization header whose JWT was signed without a `kid` in its JOSE header — e.g. tokens minted by a library that omits kid when only one key exists — while the Cube instance is configured with `jwt: { jwkUrl }`.

Common situations: Switching an existing Cube deployment from symmetric-secret (key/checkAuth) auth to JWKS auth while clients still present old tokens; an identity provider configured to not emit kid; hand-rolled token generation in tests or scripts using jsonwebtoken's sign() without keyid.

Related errors


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