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
- Verify the JWT's `kid` (decode the token header on jwt.io) and confirm a matching key exists at the configured jwkUrl.
- Correct the jwkUrl (or the jwkUrl function's tenant resolution) to point to the JWKS endpoint of the IdP that actually signs your tokens.
- Reissue client tokens after key rotation so they carry the current key's `kid`.
- Check that the JWKS endpoint is reachable and returning x5c keys (Cube logs 'JWKs Background Fetching Error' when fetching fails).
- 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
- Monitor IdP key-rotation events and reissue tokens on rotation
- Prefetch and periodically diff your JWKS endpoint against token kids in health checks
- Pin jwkUrl per tenant and add an integration test per tenant JWKS
- Keep token lifetimes short so rotated-away keys age out quickly
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
- JWT without kid inside headers
- Invalid token
- Unable to decode JWT key
- Authorization header isn't set
- API scope is missing: ${scope}
AI-assisted analysis of cube-js/cube@7d981676b3 (2026-09-02).
Data as JSON: /api/errors/1c91d8af5d992827.
Report an issue: GitHub.