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

  1. Decode the token (e.g. jwt.io) and check that sub, iat, exp and aud claims are present and non-empty
  2. Compare the token's aud claim against the broker's allowed token audiences and align configuration
  3. If isRoleClaimNotSubject is set, ensure the configured roleClaim exists in the token or add it via IdP claim mapping
  4. 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

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.