quarkusio/quarkus · warning · InvalidJwtException

EXPIRED

EXPIRED

Error message

Logout token issued to client %s expired %d seconds ago

What it means

Back-channel logout tokens (JWTs sent by the OIDC provider on logout) are validated including expiration. This InvalidJwtException with ErrorCodes.EXPIRED is thrown when the logout token's 'exp' claim plus the configured lifespan grace has passed, reporting how many seconds ago it expired.

Source

Thrown at extensions/oidc/runtime/src/main/java/io/quarkus/oidc/runtime/OidcProvider.java:210

                asymmetricKeyResolver, enforceExpReq, oidcConfig.token().issuedAtRequired());
    }

    public TokenVerificationResult verifyLogoutJwtToken(String token) throws InvalidJwtException {
        final boolean enforceExpReq = !oidcConfig.token().age().isPresent();
        TokenVerificationResult result = verifyJwtTokenInternal(token, true, false, null, ASYMMETRIC_ALGORITHM_CONSTRAINTS,
                asymmetricKeyResolver, enforceExpReq, oidcConfig.token().issuedAtRequired());
        if (!enforceExpReq) {
            // Expiry check was skipped during the initial verification but if the logout token contains the exp claim
            // then it must be verified
            final Long exp = result.localVerificationResult().getLong(Claims.exp.name());
            if (exp != null) {
                final long secondsAfterExpiry = now() / 1000 - (exp + getLifespanGrace());
                if (secondsAfterExpiry > 0) {
                    String error = "Logout token issued to client %s expired %d seconds ago".formatted(
                            oidcConfig.clientId().get(),
                            secondsAfterExpiry);
                    LOG.warn(error);
                    throw new InvalidJwtException(error, List.of(new ErrorCodeValidator.Error(ErrorCodes.EXPIRED, error)),
                            null);
                }
            }
        }
        return result;
    }

    private TokenVerificationResult verifyJwtTokenInternal(String token,
            boolean enforceAudienceVerification,
            boolean subjectRequired,
            String nonce,
            AlgorithmConstraints algConstraints,
            VerificationKeyResolver verificationKeyResolver, boolean enforceExpReq, boolean issuedAtRequired)
            throws InvalidJwtException {
        JwtConsumerBuilder builder = new JwtConsumerBuilder();

        builder.setVerificationKeyResolver(verificationKeyResolver);

View on GitHub (pinned to e1c734241f)

Solutions

  1. Increase quarkus.oidc.token.lifespan-grace to tolerate the observed delay/skew.
  2. Synchronize clocks (NTP) between the IdP and the Quarkus application hosts.
  3. If the provider retries stale logout tokens, ensure the app acknowledges/flushes the logout queue promptly.
  4. Verify with the provider why logout tokens are generated so far in the past.

Example fix

// before
# (default lifespan grace)
// after
quarkus.oidc.token.lifespan-grace=60
Defensive patterns

Strategy: try-catch

Validate before calling

Long exp = logoutToken.getLongClaimValue("exp");
if (exp != null && System.currentTimeMillis() / 1000 > exp + graceSeconds) {
    // skip processing; token already expired
}

Type guard

boolean isLogoutTokenFresh(Claims claims, long grace) {
    return claims.getExpirationTime() == null
        || System.currentTimeMillis() / 1000 <= claims.getExpirationTime() + grace;
}

Try / catch

try {
    provider.verifyLogoutJwtToken(token);
} catch (InvalidJwtException e) {
    if (e.hasError(ErrorCodes.EXPIRED)) { LOG.info("Stale logout token ignored"); return; }
    throw e;
}

Prevention

When it happens

Trigger: verifyLogoutJwtToken receives a logout token whose exp < now - getLifespanGrace(); often caused by provider retries of old logout events or large clock skew between IdP and app.

Common situations: Keycloak admin revoking sessions causing replay of stale logout tokens; app downtime delaying processing of queued logout tokens; clock drift between containers.

Related errors


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/53508f7fb2e3fa1b. Report an issue: GitHub.