{"record":{"id":"8a6a285e863424fb","repo":"apache/pulsar","slug":"expired-jwt","errorCode":"EXPIRED_JWT","errorMessage":"JWT expired: ","messagePattern":"JWT expired: ","errorType":"exception","errorClass":"AuthenticationException","httpStatus":null,"severity":"error","filePath":"pulsar-broker-auth-oidc/src/main/java/org/apache/pulsar/broker/authentication/oidc/AuthenticationProviderOpenID.java","lineNumber":461,"sourceCode":"        // The claim presence requirements are based on https://openid.net/specs/openid-connect-basic-1_0.html#IDToken\n         Verification verifierBuilder = JWT.require(alg)\n                .acceptLeeway(acceptedTimeLeewaySeconds)\n                .withAnyOfAudience(allowedAudiences)\n                .withClaimPresence(RegisteredClaims.ISSUED_AT)\n                .withClaimPresence(RegisteredClaims.EXPIRES_AT)\n                .withClaimPresence(RegisteredClaims.SUBJECT);\n\n        if (isRoleClaimNotSubject) {\n            verifierBuilder = verifierBuilder.withClaimPresence(roleClaim);\n        }\n\n        JWTVerifier verifier = verifierBuilder.build();\n\n        try {\n            return verifier.verify(jwt);\n        } catch (TokenExpiredException e) {\n            incrementFailureMetric(AuthenticationExceptionCode.EXPIRED_JWT);\n            throw new AuthenticationException(\"JWT expired: \" + e.getMessage());\n        } catch (SignatureVerificationException e) {\n            incrementFailureMetric(AuthenticationExceptionCode.ERROR_VERIFYING_JWT_SIGNATURE);\n            throw new AuthenticationException(\"JWT signature verification exception: \" + e.getMessage());\n        } catch (InvalidClaimException e) {\n            incrementFailureMetric(AuthenticationExceptionCode.INVALID_JWT_CLAIM);\n            throw new AuthenticationException(\"JWT contains invalid claim: \" + e.getMessage());\n        } catch (AlgorithmMismatchException e) {\n            incrementFailureMetric(AuthenticationExceptionCode.ALGORITHM_MISMATCH);\n            throw new AuthenticationException(\"JWT algorithm does not match Public Key algorithm: \" + e.getMessage());\n        } catch (JWTDecodeException e) {\n            incrementFailureMetric(AuthenticationExceptionCode.ERROR_DECODING_JWT);\n            throw new AuthenticationException(\"Error while decoding JWT: \" + e.getMessage());\n        } catch (JWTVerificationException | IllegalArgumentException e) {\n            incrementFailureMetric(AuthenticationExceptionCode.ERROR_VERIFYING_JWT);\n            throw new AuthenticationException(\"JWT verification failed: \" + e.getMessage());\n        }\n    }\n","sourceCodeStart":443,"sourceCodeEnd":479,"githubUrl":"https://github.com/apache/pulsar/blob/820761864ed8e2a7d2e52dd9763ad2ae117c1395/pulsar-broker-auth-oidc/src/main/java/org/apache/pulsar/broker/authentication/oidc/AuthenticationProviderOpenID.java#L443-L479","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: token fetched once, reused forever\nString jwt = fetchTokenOnce();\n// after: refresh when nearing expiry\nif (Instant.now().isAfter(expiresAt.minus(Duration.ofMinutes(5)))) {\n    jwt = fetchNewToken();\n}","handlingStrategy":"retry","validationCode":"DecodedJWT parsed = JWT.decode(jwt);\nInstant exp = Instant.ofEpochSecond(parsed.getExpiresAt().getTime() / 1000);\nif (Instant.now().isAfter(exp.minus(Duration.ofSeconds(30)))) {\n    jwt = refreshAccessToken(); // proactive refresh\n}","typeGuard":null,"tryCatchPattern":"try {\n    return verifyJWT(publicKey, publicKeyAlg, jwt);\n} catch (AuthenticationException e) {\n    if (e.getMessage().startsWith(\"JWT expired\")) {\n        jwt = refreshAccessToken(); // retry once with a fresh token\n        return verifyJWT(publicKey, publicKeyAlg, JWT.decode(jwt) instanceof DecodedJWT d ? d : null);\n    }\n    throw e;\n}","preventionTips":["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"],"tags":["auth","oidc","jwt","expired-token"],"backgroundTag":"jwt-token-expired","analyzedSha":"820761864ed8e2a7d2e52dd9763ad2ae117c1395","analyzedAt":"2026-09-06T00:14:20.138Z","contentChangedAt":"2026-09-06T00:14:20.138Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}