Skip to content

Expose JWKSourceBuilder options (cache TTL, refresh-ahead, outage tolerance) on JwkSetUriJwtDecoderBuilder #19643

Description

@fkreisEnbw

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) plus builder.restOperations(apacheHttpClient5). Gets me a 6 hour TTL and one retry, but no background refresh, and outage tolerance only through a Cache subclass that swallows
    ValueRetrievalException — which works solely because SpringJWKSource.getJWKSet returns 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

in: oauth2An issue in OAuth2 modules (oauth2-core, oauth2-client, oauth2-resource-server, oauth2-jose)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions