apache/pulsar · error · AuthenticationException

EXPIRED_JWT

EXPIRED_JWT

Error message

JWT expired: 

What it means

The JWT's 'exp' claim is in the past (beyond the configured accepted time leeway), so java-jwt's verifier throws TokenExpiredException during verifier.verify(jwt). The provider maps it to AuthenticationException(EXPIRED_JWT) and records a failure metric. This is a deliberate, precise signal that the credential itself is validly formed and signed but no longer valid in time.

Source

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

        // The claim presence requirements are based on https://openid.net/specs/openid-connect-basic-1_0.html#IDToken
         Verification verifierBuilder = JWT.require(alg)
                .acceptLeeway(acceptedTimeLeewaySeconds)
                .withAnyOfAudience(allowedAudiences)
                .withClaimPresence(RegisteredClaims.ISSUED_AT)
                .withClaimPresence(RegisteredClaims.EXPIRES_AT)
                .withClaimPresence(RegisteredClaims.SUBJECT);

        if (isRoleClaimNotSubject) {
            verifierBuilder = verifierBuilder.withClaimPresence(roleClaim);
        }

        JWTVerifier verifier = verifierBuilder.build();

        try {
            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());
        }
    }

View on GitHub (pinned to 820761864e)

Solutions

  1. Refresh the client's OIDC token (re-run the authorization/token flow) and reconnect; ensure the client refreshes proactively before exp
  2. Verify clock sync (NTP) between client, broker, and identity provider to eliminate skew at token boundaries
  3. Increase the provider's acceptedTimeLeewaySeconds (if a small skew is expected) — but prefer real refresh over large leeway
  4. Extend the token lifetime at the IdP if tokens expire during long jobs, and implement automatic renewal in the client

Example fix

// before: token fetched once, reused forever
String jwt = fetchTokenOnce();
// after: refresh when nearing expiry
if (Instant.now().isAfter(expiresAt.minus(Duration.ofMinutes(5)))) {
    jwt = fetchNewToken();
}
Defensive patterns

Strategy: retry

Validate before calling

DecodedJWT parsed = JWT.decode(jwt);
Instant exp = Instant.ofEpochSecond(parsed.getExpiresAt().getTime() / 1000);
if (Instant.now().isAfter(exp.minus(Duration.ofSeconds(30)))) {
    jwt = refreshAccessToken(); // proactive refresh
}

Try / catch

try {
    return verifyJWT(publicKey, publicKeyAlg, jwt);
} catch (AuthenticationException e) {
    if (e.getMessage().startsWith("JWT expired")) {
        jwt = refreshAccessToken(); // retry once with a fresh token
        return verifyJWT(publicKey, publicKeyAlg, JWT.decode(jwt) instanceof DecodedJWT d ? d : null);
    }
    throw e;
}

Prevention

When it happens

Trigger: verifier.verify(jwt) in verifyJWT() throws TokenExpiredException: the client presented a token whose exp claim has passed and the skew (acceptedTimeLeewaySeconds) does not cover the difference.

Common situations: Long-lived broker/client connection reusing a cached token past its expiry; client clock skewed ahead of the IdP so the token looks expired on the broker; client fetched a token but delayed startup; token lifetime configured too short on the IdP for the workload's refresh cadence.

Related errors


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