spring-projects / spring-projects/spring-security
Expose JWKSourceBuilder options (cache TTL, refresh-ahead, outage tolerance) on JwkSetUriJwtDecoderBuilder
@jzheaux is already working on this.
Since Sep 3, 2026.
- Dominant language
- Java
- Stars
- 9.6k
- Forks
- 6.3k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 52
Description
Expected Behavior
JwkSetUriJwtDecoderBuilder should let me reach the JWKSourceBuilder features it builds on — refresh-ahead caching, cache TTL, outage tolerance, rate limiting — e.g. a
jwkSourceBuilderCustomizer(Consumer<JWKSourceBuilder<SecurityContext>>) alongside the existing jwtProcessorCustomizer.
I'd go further: refresh-ahead caching and outage tolerance are Nimbus's own defaults in JWKSourceBuilder, and I'd argue they are the right defaults here too. A key set is needed for every request, changes a few times a year,
and the fetch runs on a user request thread while SpringJWKSource holds a ReentrantLock — so refreshing ahead of expiry and tolerating a failed refetch is what most resource servers want. Today all of it is switched off,
and none of it is configurable.
Current Behavior
jwkSource() hardcodes .refreshAheadCache(false).rateLimited(false) and installs Nimbus's cache only when no Cache was supplied (7.1.1):
JWKSourceBuilder.create(new SpringJWKSource<>(this.restOperations, this.cache, jwkSetUri))
.refreshAheadCache(false)
.rateLimited(false)
.cache(this.cache instanceof NoOpCache)
.build();
With no Cache set — the default — that means JWKSourceBuilder.DEFAULT_CACHE_TIME_TO_LIVE of 5 minutes with no way to change it: roughly 288 blocking fetches per day per instance, each paid by whichever user request finds
the entry expired. Supplying a Cache fixes the TTL, but refresh-ahead and outage tolerance stay unreachable: a JwkSetUriJwtDecoderBuilderCustomizer has no seam for them and SpringJWKSource is private.
#17226 asked for this and was closed as a duplicate of #17046. #17046 added NimbusJwtDecoder.withJwkSource(...), which doesn't cover it: JwkSourceJwtDecoderBuilder takes no jwkSetUri, so that route means giving up OIDC
discovery of the key set URI (JwtDecoderProviderConfigurationUtils is package private) and the issuer/audience validator wiring that Spring Boot's auto-configuration does for me.
Context
Azure Entra as the provider, two AWS environments, Spring Boot 4.1 / Spring Security 7.1.1. In 14 days of logs: ~8000 JWK Set fetches on the 5-minute grid, 49 of them failing with a transient java.net.SocketException: Connection reset during the TLS handshake. A failure on an expired entry isn't a 401 — it becomes an AuthenticationServiceException, which AuthenticationEntryPointFailureHandler rethrows, so it surfaces as an HTTP 500.
Alternatives I tried:
builder.cache(caffeineCache)plusbuilder.restOperations(apacheHttpClient5). Gets me a 6 hour TTL and one retry, but no background refresh, and outage tolerance only through aCachesubclass that swallows
ValueRetrievalException— which works solely becauseSpringJWKSource.getJWKSetreturns its own field rather than the cache's value.NimbusJwtDecoder.withJwkSource(...)with a properly layered Nimbus source: around 230 lines of reimplemented discovery and validator wiring to get one feature.
Both are workarounds for something the underlying library already does.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.