cube-js/cube · error · CubejsHandlerError

Unable to verify, JWK with kid: "${decoded.header.kid}" not

Error message

Unable to verify, JWK with kid: "${decoded.header.kid}" not found

What it means

With `jwkUrl` configured, Cube fetches the JWKS document and looks up the signing key whose `kid` matches the token's header `kid`. If no such key is found in the JWKS response, verification cannot proceed and this 403 Forbidden error is thrown. Note it is only thrown when the lookup returns falsy — e.g. key rotation has removed the old key.

Source

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

            '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`
          );
        }

        return verifyToken(auth, jwk);
      };
    }

    return async (req, auth) => {
      if (auth) {
        try {
          req.securityContext = await checkAuthFn(auth);
          req.signedWithPlaygroundAuthSecret =
            Boolean(internalOptions?.isPlaygroundCheckAuth) && hasDevTokenScope(req.securityContext);
        } catch (e: any) {
          if (this.enforceSecurityChecks) {

View on GitHub (pinned to 7d981676b3)

Solutions

  1. Verify the JWT's `kid` (decode the token header on jwt.io) and confirm a matching key exists at the configured jwkUrl.
  2. Correct the jwkUrl (or the jwkUrl function's tenant resolution) to point to the JWKS endpoint of the IdP that actually signs your tokens.
  3. Reissue client tokens after key rotation so they carry the current key's `kid`.
  4. Check that the JWKS endpoint is reachable and returning x5c keys (Cube logs 'JWKs Background Fetching Error' when fetching fails).
  5. Clear/restart to refresh cached JWKs if a newly published key has not been picked up.

Example fix

// before — wrong JWKS URL (legacy endpoint)
jwt: { jwkUrl: 'https://app.example.com/.well-known/jwks.json' }
// after — correct tenant JWKS
jwt: { jwkUrl: 'https://app.example.com/auth/realms/main/protocol/openid-connect/certs' }
Defensive patterns

Strategy: validation

Validate before calling

async function kidExistsInJwks(token, jwkUrl) {
  const { header } = jwt.decode(token, { complete: true });
  const jwks = await (await fetch(jwkUrl)).json();
  return jwks.keys.some(k => k.kid === header.kid);
}
if (!(await kidExistsInJwks(token, jwkUrl))) throw new Error(`kid not published in JWKS`);

Type guard

function jwksHasKid(jwks, kid) {
  return Array.isArray(jwks?.keys) && jwks.keys.some(k => k.kid === kid);
}

Try / catch

try {
  await cubeApi.load(query);
} catch (e) {
  if (e.status === 403 && /JWK with kid/.test(e.message)) {
    // refresh token from IdP / re-check jwkUrl tenant mapping
  }
  throw e;
}

Prevention

When it happens

Trigger: A JWT whose header `kid` (e.g. "stale-key") is absent from the JSON returned by the configured JWKS URL; or the JWKS URL itself returns an empty/incorrect key set (wrong tenant, wrong realm, partial page).

Common situations: IdP key rotation after which old tokens signed with a retired key are still presented; pointing jwkUrl at the wrong Auth0/Cognito/Keycloak endpoint; multi-tenant setups where jwkUrl() resolves to a different tenant's JWKS than the one that signed the token; JWKS caching holding an outdated key set.

Related errors


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