apache/pulsar · error · AuthenticationException
ERROR_VERIFYING_JWT
ERROR_VERIFYING_JWT
Error message
JWT verification failed:
What it means
Thrown by verifyJWT when java-jwt raises a generic JWTVerificationException or an IllegalArgumentException during verifier.verify(jwt) — any verification failure not covered by the more specific expired/signature/invalid-claim/algorithm-mismatch/decode handlers. The provider increments the ERROR_VERIFYING_JWT failure metric and appends the library's message.
Source
Thrown at pulsar-broker-auth-oidc/src/main/java/org/apache/pulsar/broker/authentication/oidc/AuthenticationProviderOpenID.java:476
return verifier.verify(jwt);
} catch (TokenExpiredException e) {
incrementFailureMetric(AuthenticationExceptionCode.EXPIRED_JWT);
throw new AuthenticationException("JWT expired: " + e.getMessage());
} catch (SignatureVerificationException e) {
incrementFailureMetric(AuthenticationExceptionCode.ERROR_VERIFYING_JWT_SIGNATURE);
throw new AuthenticationException("JWT signature verification exception: " + e.getMessage());
} catch (InvalidClaimException e) {
incrementFailureMetric(AuthenticationExceptionCode.INVALID_JWT_CLAIM);
throw new AuthenticationException("JWT contains invalid claim: " + e.getMessage());
} catch (AlgorithmMismatchException e) {
incrementFailureMetric(AuthenticationExceptionCode.ALGORITHM_MISMATCH);
throw new AuthenticationException("JWT algorithm does not match Public Key algorithm: " + e.getMessage());
} catch (JWTDecodeException e) {
incrementFailureMetric(AuthenticationExceptionCode.ERROR_DECODING_JWT);
throw new AuthenticationException("Error while decoding JWT: " + e.getMessage());
} catch (JWTVerificationException | IllegalArgumentException e) {
incrementFailureMetric(AuthenticationExceptionCode.ERROR_VERIFYING_JWT);
throw new AuthenticationException("JWT verification failed: " + e.getMessage());
}
}
/**
* Validate the configured allow list of allowedIssuers. The allowedIssuers set must be nonempty in order for
* the plugin to authenticate any token. Thus, it fails initialization if the configuration is
* missing. Each issuer URL should use the HTTPS scheme. The plugin fails initialization if any
* issuer url is insecure, unless requireHttps is false.
* @param allowedIssuers - issuers to validate
* @param requireHttps - whether to require https for issuers.
* @param allowEmptyIssuers - whether to allow empty issuers. This setting only makes sense when kubernetes is used
* as a fallback issuer.
* @return the validated issuers
* @throws IllegalArgumentException if the allowedIssuers is empty, or contains insecure issuers when required
*/
private Set<String> validateIssuers(Set<String> allowedIssuers, boolean requireHttps, boolean allowEmptyIssuers) {
if (allowedIssuers == null || (allowedIssuers.isEmpty() && !allowEmptyIssuers)) {
throw new IllegalArgumentException("Missing configured value for: " + ALLOWED_TOKEN_ISSUERS);View on GitHub (pinned to 820761864e)
Solutions
- Decode the token (e.g. jwt.io) and check that sub, iat, exp and aud claims are present and non-empty
- Compare the token's aud claim against the broker's allowed token audiences and align configuration
- If isRoleClaimNotSubject is set, ensure the configured roleClaim exists in the token or add it via IdP claim mapping
- Enable broker debug logging of AuthenticationException messages to see the exact java-jwt message for the failure
Example fix
// before (broker requires custom claim the IdP does not issue) authenticationProviderOpenID.roleClaim="custom_role"; // after (map the claim in the IdP or use the default subject role) authenticationProviderOpenID.roleClaim="roles"; // and map roles claim in IdP
Defensive patterns
Strategy: validation
Validate before calling
// Pre-check the token's required claims before presenting it to the broker
DecodedJWT jwt = JWT.decode(token);
if (jwt.getSubject() == null || jwt.getIssuedAt() == null || jwt.getExpiresAt() == null
|| jwt.getAudience() == null || jwt.getAudience().isEmpty()) {
throw new IllegalStateException("Token missing required claims: sub, iat, exp, aud");
}
// and confirm the aud claim matches an allowed broker audience Try / catch
try {
authentication.authenticate(authDataSource);
} catch (AuthenticationException e) {
if (e.getMessage() != null && e.getMessage().startsWith("JWT verification failed")) {
// inspect claims: missing sub/iat/exp, absent role claim, or audience mismatch
throw new IllegalStateException("Token rejected: " + e.getMessage(), e);
} else {
throw e;
}
} Prevention
- Configure the IdP to include sub, iat, exp and aud in every ID token (per the OIDC basic spec)
- Keep the broker's roleClaim setting in sync with claims actually present in tokens
- Align client audience and broker allowedTokenAudiences after any IdP tenant/app change
- Decode a sample token after IdP upgrades to detect removed or renamed claims early
When it happens
Trigger: verification fails on claim-presence checks configured via withClaimPresence (iss, sub, iat, exp or the custom role claim missing), withAnyOfAudience rejection with an IllegalArgumentException, or other JWTVerificationException subclasses not explicitly caught.
Common situations: ID token lacks the subject (sub) or issued-at (iat) claim; the roleClaim configured on the broker is absent from tokens issued by the IdP; audience claim does not match tokenAudience/allowedAudiences configuration after an IdP or client-audience change; token clock fields set to zero or negative values.
Related errors
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/a15b4f4f6eed8203.
Report an issue: GitHub.