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
- 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.
- Verify the configured issuer URL points to the same IdP/environment that issued the token.
- Tune oidcCacheMissRefreshSeconds (KEY_ID_CACHE_MISS_REFRESH_SECONDS) down so unknown kids trigger a JWKS reload sooner after key rotation.
- If rotation lag is the cause, wait for cache expiry/refresh or restart the broker to force a fresh JWKS fetch, then retry authentication.
- 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
- Set oidcCacheMissRefreshSeconds low enough to pick up IdP key rotation quickly.
- Keep cache expiration/refresh settings shorter than the IdP's key rotation cadence.
- Decode failing tokens and cross-check their kid against the live JWKS endpoint.
- Ensure the issuer URL points at the same IdP environment that issued the tokens.
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.