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
- Refresh the client's OIDC token (re-run the authorization/token flow) and reconnect; ensure the client refreshes proactively before exp
- Verify clock sync (NTP) between client, broker, and identity provider to eliminate skew at token boundaries
- Increase the provider's acceptedTimeLeewaySeconds (if a small skew is expected) — but prefer real refresh over large leeway
- 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
- Refresh tokens on a schedule well before exp (e.g., at 80% of lifetime)
- Run NTP everywhere; treat repeated expiries as clock-skew evidence
- Keep token lifetimes long enough for job duration, or renew mid-job
- Cache the leeway configuration; avoid shrinking it below realistic latency
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
- ERROR_DECODING_JWT
- UNSUPPORTED_ALGORITHM
- ALGORITHM_MISMATCH
- ERROR_VERIFYING_JWT_SIGNATURE
- INVALID_JWT_CLAIM
AI-assisted analysis of apache/pulsar@820761864e (2026-09-06).
Data as JSON: /api/errors/8a6a285e863424fb.
Report an issue: GitHub.