Change in Quarkus SmallRye JWT expiration tolerance leads to downstream expiry state inconsistency
0 reputation · 12 Jun 2021, 18:51 UTC
0 reputation · 12 Jun 2021, 18:51 UTC
Goal: When a JWT is accepted under the configured expiration tolerance, the application should treat the token as non‑expired for all internal purposes, including downstream propagation to identity providers or audit systems, so that services relying on the token’s expiry state see a consistent view.
Constraint: In Quarkus 3.2 and later the property quarkus.jwt.token.expire-tolerance permits tokens that are a few seconds past their exp claim to be accepted, but the JsonWebToken.getExp() value remains in the past, causing logs, introspection calls, or role‑propagation to see the token as expired even though the request succeeded.
Uncertainty: It is unclear whether this split between acceptance and internal expiry is an intentional security‑by‑design choice or an oversight, and whether there exists a configuration or extension that can make the internal expiry state reflect the tolerance without weakening signature, issuer, or audience validation.
Questions: Is the downstream expiry state deliberately kept as expired despite tolerance? Can the tolerance be applied to the internal expiry state used by JsonWebToken and related propagation mechanisms? What configuration or custom handler would align the internal expiry with the tolerance while preserving strict validation of other claims?
30775 reputation · 12 Jun 2021, 22:14 UTC
Yes, this split is expected behavior. In Quarkus 3.2+, quarkus.jwt.token.expire-tolerance widens only the acceptance check performed by the SmallRye JWT validator. It does not rewrite the token's claims. JsonWebToken.getExp() (and the raw claim set used for propagation) still returns the original exp value, so anything inspecting the token after validation — logs, audit code, token propagation to another service — will see a timestamp in the past even though the request was admitted.
Confirmed: the tolerance is applied at validation time only; the parsed claim set is passed through unchanged. There is no built-in property that shifts the internal exp value to match the tolerance.
Likely explanation (not officially documented as design intent): keeping the original claim preserves auditability and avoids silently extending a token's effective lifetime in places the framework cannot see. If Quarkus rewrote exp, a propagated token would appear valid downstream for longer than the issuer intended, which is arguably worse than the inconsistency you're hitting. So treat this as security-by-design rather than an oversight, and assume it will not change.
There is no configuration to align the internal expiry with the tolerance. Your options, in order of preference:
getExp() against now - tolerance instead of now. This keeps the raw claim intact and makes your internal view consistent with the acceptance decision.JsonWebToken (e.g., via a CDI decorator or a request-scoped bean) and expose an adjusted expiry — exp + tolerance — only for your internal consumers. Do not mutate the underlying claim set used for propagation.Set a known tolerance and confirm the split directly:
quarkus.jwt.token.expire-tolerance=10Issue a JWT with exp five seconds in the past. The request should succeed while a log of jwt.getExpirationTime() shows a past instant. Then issue one with exp 15 seconds in the past — it must be rejected with 401. If you add a decorator, repeat the first test and confirm your internal view reports a future expiry while the raw claim is unchanged.
Keep the tolerance small (seconds, not minutes). Any wrapper that makes an expired token look valid internally can weaken replay detection in your own audit trail, and must never bypass signature, issuer, or audience validation — those are unaffected by this property and should stay that way.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.