{"record":{"id":"c20d20609e1e6da8","repo":"quarkusio/quarkus","slug":"jwt-svid-from-spire-agent-is-missing-the-required-c20d20","errorCode":null,"errorMessage":"JWT-SVID from SPIRE agent is missing the required 'exp' claim","messagePattern":"JWT-SVID from SPIRE agent is missing the required 'exp' claim","errorType":"exception","errorClass":"SpiffeConnectionException","httpStatus":null,"severity":"error","filePath":"extensions/spiffe-client/runtime/src/main/java/io/quarkus/spiffe/client/runtime/internal/SpiffeClientImpl.java","lineNumber":269,"sourceCode":"            audience = Set.of(audienceAsString);\n        } else {\n            throw new SpiffeConnectionException(\n                    \"JWT-SVID 'aud' claim is not a string or array of strings\");\n        }\n        if (!audience.containsAll(requestedAudiences)) {\n            throw new SpiffeConnectionException(\n                    \"JWT-SVID 'aud' claim does not contain the requested audiences; requested: \"\n                            + requestedAudiences + \", received: \" + audience);\n        }\n        if (audience.size() != requestedAudiences.size()) {\n            throw new SpiffeConnectionException(\n                    \"JWT-SVID 'aud' claim contains unexpected extra audiences; requested: \"\n                            + requestedAudiences + \", received: \" + audience);\n        }\n\n        Long exp = payload.getLong(\"exp\");\n        if (exp == null) {\n            throw new SpiffeConnectionException(\"JWT-SVID from SPIRE agent is missing the required 'exp' claim\");\n        }\n        Instant expiry = Instant.ofEpochSecond(exp);\n        if (expiry.isBefore(Instant.now())) {\n            throw new SpiffeConnectionException(\"JWT-SVID from SPIRE agent is already expired\");\n        }\n\n        record WorkloadJsonWebTokenImpl(String token, String subject, Set<String> audience,\n                Instant expiry) implements WorkloadJsonWebToken {\n        }\n        return new WorkloadJsonWebTokenImpl(token, sub, Set.copyOf(audience), expiry);\n    }\n\n    private static WorkloadCertificateDocument toWorkloadCertificate(X509SVIDResponse response)\n            throws SpiffeConnectionException {\n        List<X509SVID> svids = response.getSvidsList();\n        if (svids.isEmpty()) {\n            throw new SpiffeConnectionException(\"X.509-SVID response from SPIRE agent contains no SVIDs\");\n        }","sourceCodeStart":251,"sourceCodeEnd":287,"githubUrl":"https://github.com/quarkusio/quarkus/blob/e1c734241f34c7919086ceb4c9262b4a58f6de44/extensions/spiffe-client/runtime/src/main/java/io/quarkus/spiffe/client/runtime/internal/SpiffeClientImpl.java#L251-L287","documentation":"A JWT-SVID must include an 'exp' (expiry) claim so the client can determine validity; a missing exp makes lifetime handling impossible and is rejected. Additionally, if the token is already expired the client throws 'JWT-SVID from SPIRE agent is already expired' from the same block.","triggerScenarios":"Payload lacks the exp claim, or exp is in the past (Instant.ofEpochSecond(exp).isBefore(Instant.now())) — e.g. reusing cached tokens, clock skew between host and SPIRE agent, or stubs without exp.","commonSituations":"NIST/clock skew in containers or VMs causing freshly issued tokens to appear expired; caching a WorkloadJsonWebToken past its TTL; synthetic test tokens missing standard claims.","solutions":["Synchronize clocks (NTP/chrony) across the workload host and SPIRE agent and retry token fetch","Re-fetch the JWT-SVID instead of reusing a previously obtained token; refresh on each expiry window","Update test fixtures to include a future exp value"],"exampleFix":"// before\n{\"sub\":\"spiffe://example.org/workload\",\"aud\":[\"api\"]}\n// after\n{\"sub\":\"spiffe://example.org/workload\",\"aud\":[\"api\"],\"exp\":1757000000}","handlingStrategy":"retry","validationCode":"// check token TTL before reuse\nlong maxAgeSeconds = 300; // typical SPIRE default\nboolean likelyFresh = fetchedAtEpoch + maxAgeSeconds > Instant.now().getEpochSecond();","typeGuard":null,"tryCatchPattern":"try {\n    return spiffeClient.getWorkloadJsonWebToken(audiences).await().indefinitely();\n} catch (SpiffeConnectionException e) {\n    if (e.getMessage().contains(\"already expired\") || e.getMessage().contains(\"'exp'\")) {\n        return spiffeClient.getWorkloadJsonWebToken(audiences).await().indefinitely(); // re-fetch\n    }\n    throw e;\n}","preventionTips":["Run NTP on hosts and containers","Re-fetch tokens instead of caching past exp","Refresh at 80% of TTL"],"tags":["spiffe","jwt","expired-token","missing-claim"],"backgroundTag":"jwt-token-expired","analyzedSha":"e1c734241f34c7919086ceb4c9262b4a58f6de44","analyzedAt":"2026-09-05T17:01:29.979Z","contentChangedAt":"2026-09-05T17:01:29.979Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}