apache/pulsar · error · IllegalArgumentException

ERROR_RETRIEVING_PUBLIC_KEY

ERROR_RETRIEVING_PUBLIC_KEY

Error message

No JWK found for Key ID 

What it means

JwksCache.getJwkForKID() scans the cached JWK Set (fetched from the issuer's jwks_uri or the Kubernetes API server) for a JWK whose kid matches the JWT header's key ID. When none matches it increments the ERROR_RETRIEVING_PUBLIC_KEY failure metric and throws IllegalArgumentException("No JWK found for Key ID <kid>"). getJwkAndMaybeReload() first retries with a fresh JWKS fetch (rate-limited by keyIdCacheMissRefreshSeconds) before propagating the failure, so this usually means the key is genuinely absent even after a reload.

Source

Thrown at pulsar-broker-auth-oidc/src/main/java/org/apache/pulsar/broker/authentication/oidc/JwksCache.java:223

                }
            });
        } catch (ApiException e) {
            authenticationProvider.incrementFailureMetric(ERROR_RETRIEVING_PUBLIC_KEY);
            future.completeExceptionally(
                    new AuthenticationException("Failed to retrieve public key from Kubernetes API server: "
                            + e.getMessage()));
        }
        return future;
    }

    private Jwk getJwkForKID(Optional<String> maybeJwksUri, List<Jwk> jwks, String keyId) {
        for (Jwk jwk : jwks) {
            if (jwk.getId().equals(keyId)) {
                return jwk;
            }
        }
        authenticationProvider.incrementFailureMetric(ERROR_RETRIEVING_PUBLIC_KEY);
        throw new IllegalArgumentException("No JWK found for Key ID " + keyId);
    }

    /**
     * The JWK Set is stored in the "keys" key see https://www.rfc-editor.org/rfc/rfc7517#section-5.1.
     *
     * @param jwksUri - the URI used to retrieve the JWKS
     * @param jwks - the JWKS to convert
     * @return a list of {@link Jwk}
     */
    private List<Jwk> convertToJwks(String jwksUri, Map<String, Object> jwks) throws AuthenticationException {
        try {
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> jwkList = (List<Map<String, Object>>) jwks.get("keys");
            final List<Jwk> result = new ArrayList<>();
            for (Map<String, Object> jwk : jwkList) {
                result.add(Jwk.fromValues(jwk));
            }
            return result;

View on GitHub (pinned to 820761864e)

Solutions

  1. Confirm the token's kid exists in the issuer's JWKS: decode the JWT header (e.g. jq -R 'split(".")[0] | @base64d') and compare with the keys at the configured jwks_uri.
  2. Verify the configured issuer URL points to the same IdP/environment that issued the token.
  3. Tune oidcCacheMissRefreshSeconds (KEY_ID_CACHE_MISS_REFRESH_SECONDS) down so unknown kids trigger a JWKS reload sooner after key rotation.
  4. If rotation lag is the cause, wait for cache expiry/refresh or restart the broker to force a fresh JWKS fetch, then retry authentication.
  5. Ensure the IdP still publishes the signing key used for the token (it may have removed old keys after rotation).

Example fix

// broker.conf
// before (slow to pick up rotated keys)
oidcCacheMissRefreshSeconds=3600
// after (refresh JWKS quickly on unknown kid)
oidcCacheMissRefreshSeconds=60
Defensive patterns

Strategy: retry

Validate before calling

// Before configuring, confirm the token's kid is published:
// kid=$(echo $JWT | cut -d. -f1 | base64 -d 2>/dev/null | jq -r .kid)
// curl -s $JWKS_URI | jq -e --arg kid "$kid" '.keys[] | select(.kid == $kid)'

Try / catch

future.exceptionally(ex -> {
    if (ex.getCause() instanceof IllegalArgumentException
            && ex.getCause().getMessage().startsWith("No JWK found for Key ID")) {
        // force JWKS refresh / re-fetch token with a current kid, then retry once
    }
    return null;
});

Prevention

When it happens

Trigger: A JWT whose kid header does not match any key published in the issuer's JWKS; the JWKS was cached before the IdP rotated keys and the cache-miss refresh window (keyIdCacheMissRefreshSeconds) has not elapsed; the broker is pointed at the wrong issuer/jwks_uri so its key set never contains the token's kid; the token's kid header is missing or empty; in Kubernetes mode, the service-account token's kid is not in the keyset served by the API server.

Common situations: IdP key rotation (e.g. Auth0/Keycloak rotating signing keys) faster than the configured refresh interval; misconfigured afdGw / configurationError where authenticationProvider OpenID is given the wrong issuer URL; multiple environments (staging vs prod IdP) mixed up so tokens from one are validated against the other's JWKS; tokens signed with a key the IdP has since unpublished.

Related errors


AI-assisted analysis of apache/pulsar@820761864e (2026-09-06). Data as JSON: /api/errors/60281078ddaa72f7. Report an issue: GitHub.