1
0
mirror of synced 2026-08-04 17:27:13 +00:00

Increase Default NimbusJwtDecoder Timeouts to 30 Seconds

NimbusJwtDecoder's default RestOperations now respects the JDK's
sun.net.client.defaultConnectTimeout/defaultReadTimeout system properties,
falling back to 30 seconds instead of the previous 500 milliseconds,
matching JwtDecoderProviderConfigurationUtils's existing behavior.

Also documents this default and the RestOperations override in the
reference guide and migration guide (the reference guide's existing
"Configuring Timeouts" section already claimed 30 seconds -- it's been
inaccurate since the 500ms default shipped and is now correct again), and
documents providing a custom JwtDecoderFactory<ClientRegistration> for
OAuth2 Login's ID Token decoding.

Issue gh-19474

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
Josh Cummings
2026-08-03 15:51:41 -06:00
parent 1eef373ca0
commit 0f6f453ea0
7 changed files with 139 additions and 9 deletions
@@ -132,4 +132,11 @@ authenticationConverter.setBearerTokenResolver(myBearerTokenResolver)
authenticationConverter.setAuthenticationDetailsSource(myAuthenticationDetailsSource)
val filter = BearerTokenAuthenticationFilter(authenticationManager, authenticationConverter)
----
== `NimbusJwtDecoder`'s Default Connect and Read Timeouts Are Now 30 Seconds
`NimbusJwtDecoder`'s default `RestOperations`, used to fetch a JWK Set when no `RestOperations` is otherwise configured, previously used a 500 millisecond connect and read timeout.
This value was too short for many deployments and is now 30 seconds, aligning with the connect and read timeouts already used elsewhere in the OAuth2 Client and Resource Server support, such as xref:servlet/oauth2/client/index.adoc[`ClientRegistrations`].
If your application relies on the previous, shorter timeout -- for example, expecting a fast failure when the authorization server is unreachable -- you can restore it either by setting the JDK's `sun.net.client.defaultConnectTimeout` and `sun.net.client.defaultReadTimeout` system properties (in milliseconds), or by providing your own `RestOperations`, as described in xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-timeouts[Configuring Timeouts].
======
@@ -944,5 +944,52 @@ For MAC-based algorithms (such as `HS256`, `HS384`, or `HS512`), the `client-sec
If more than one `ClientRegistration` is configured for OpenID Connect 1.0 Authentication, the JWS algorithm resolver may evaluate the provided `ClientRegistration` to determine which algorithm to return.
====
[[oauth2login-advanced-idtoken-decoder-factory]]
== Providing a Custom JwtDecoderFactory
`OidcIdTokenDecoderFactory` is deliberately narrow: it exists to make it easy to support multiple `ClientRegistration` instances, each potentially needing a different `JwtDecoder`, and its configuration surface is limited to what most applications need for that purpose, such as the JWS algorithm resolver shown above.
If your application needs deeper control over how the ID Token's `JwtDecoder` is constructed -- for example, providing your own `RestOperations` -- you can instead provide your own `JwtDecoderFactory<ClientRegistration>` `@Bean`.
Because OAuth2 Login does not use a resource server `JwtDecoder` bean for this purpose, this is the supported way to reach the same level of control that `NimbusJwtDecoder`'s builder already offers, without introducing additional configuration properties on `OidcIdTokenDecoderFactory` itself.
For a single `ClientRegistration`, this can be as simple as:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory(RestOperations rest) {
return (clientRegistration) -> {
String issuerUri = clientRegistration.getProviderDetails().getIssuerUri();
NimbusJwtDecoder decoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).restOperations(rest).build();
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(new OidcIdTokenValidator(clientRegistration)));
decoder.setClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter());
return decoder;
};
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun idTokenDecoderFactory(rest: RestOperations): JwtDecoderFactory<ClientRegistration> {
return JwtDecoderFactory { clientRegistration ->
val issuerUri = clientRegistration.providerDetails.issuerUri
val decoder = NimbusJwtDecoder.withIssuerLocation(issuerUri).restOperations(rest).build()
decoder.setJwtValidator(JwtValidators.createDefaultWithValidators(OidcIdTokenValidator(clientRegistration)))
decoder.setClaimTypeConverter(OidcIdTokenDecoderFactory.createDefaultClaimTypeConverter())
decoder
}
}
----
======
If you have multiple `ClientRegistration` instances and want to avoid rebuilding a `JwtDecoder` on every request, cache the decoder per registration, for example by annotating the factory method with `@Cacheable` or by keying a `Map` on `ClientRegistration#getRegistrationId()`.
[[oauth2login-advanced-oidc-logout]]
Then, you can proceed to configure xref:servlet/oauth2/login/logout.adoc[logout]
@@ -1582,6 +1582,7 @@ fun jwtDecoder(): JwtDecoder {
== Configuring Timeouts
By default, Resource Server uses connection and socket timeouts of 30 seconds each for coordinating with the authorization server.
You can override these defaults without changing any code by setting the JDK's `sun.net.client.defaultConnectTimeout` and `sun.net.client.defaultReadTimeout` system properties (in milliseconds).
This may be too short in some scenarios.
Further, it doesn't take into account more sophisticated patterns like back-off and discovery.
@@ -66,11 +66,9 @@ final class JwtDecoderProviderConfigurationUtils {
private static final RestTemplate rest = new RestTemplate();
static {
int connectTimeout = Integer.parseInt(System.getProperty("sun.net.client.defaultConnectTimeout", "30000"));
int readTimeout = Integer.parseInt(System.getProperty("sun.net.client.defaultReadTimeout", "30000"));
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(connectTimeout);
requestFactory.setReadTimeout(readTimeout);
requestFactory.setConnectTimeout(getConnectTimeout());
requestFactory.setReadTimeout(getReadTimeout());
rest.setRequestFactory(requestFactory);
}
@@ -80,6 +78,27 @@ final class JwtDecoderProviderConfigurationUtils {
private JwtDecoderProviderConfigurationUtils() {
}
/**
* Returns the default HTTP connect timeout, in milliseconds, for fetching
* provider/JWK Set metadata. Honors the JDK's
* {@code sun.net.client.defaultConnectTimeout} system property when set, otherwise
* defaults to 30 seconds.
* @return the default HTTP connect timeout, in milliseconds
*/
static int getConnectTimeout() {
return Integer.parseInt(System.getProperty("sun.net.client.defaultConnectTimeout", "30000"));
}
/**
* Returns the default HTTP read timeout, in milliseconds, for fetching provider/JWK
* Set metadata. Honors the JDK's {@code sun.net.client.defaultReadTimeout} system
* property when set, otherwise defaults to 30 seconds.
* @return the default HTTP read timeout, in milliseconds
*/
static int getReadTimeout() {
return Integer.parseInt(System.getProperty("sun.net.client.defaultReadTimeout", "30000"));
}
static Map<String, Object> getConfigurationForOidcIssuerLocation(String oidcIssuerLocation) {
return getConfiguration(oidcIssuerLocation, rest, oidc(oidcIssuerLocation));
}
@@ -296,7 +296,7 @@ public final class NimbusJwtDecoder implements JwtDecoder {
private final Set<SignatureAlgorithm> signatureAlgorithms = new HashSet<>();
private RestOperations restOperations = new RestTemplateWithNimbusDefaultTimeouts();
private RestOperations restOperations = new RestTemplateWithDefaultTimeouts();
private Cache cache = new NoOpCache("default");
@@ -568,12 +568,12 @@ public final class NimbusJwtDecoder implements JwtDecoder {
* A RestTemplate with timeouts configured to avoid blocking indefinitely when
* fetching JWK Sets while holding the reentrantLock.
*/
private static final class RestTemplateWithNimbusDefaultTimeouts extends RestTemplate {
private static final class RestTemplateWithDefaultTimeouts extends RestTemplate {
private RestTemplateWithNimbusDefaultTimeouts() {
private RestTemplateWithDefaultTimeouts() {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT);
requestFactory.setReadTimeout(JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT);
requestFactory.setConnectTimeout(JwtDecoderProviderConfigurationUtils.getConnectTimeout());
requestFactory.setReadTimeout(JwtDecoderProviderConfigurationUtils.getReadTimeout());
setRequestFactory(requestFactory);
}
@@ -30,6 +30,7 @@ import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jose.util.Base64URL;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.security.oauth2.jose.TestKeys;
@@ -46,6 +47,40 @@ import static org.mockito.BDDMockito.mock;
public class JwtDecoderProviderConfigurationUtilsTests {
@AfterEach
public void cleanup() {
System.clearProperty("sun.net.client.defaultConnectTimeout");
System.clearProperty("sun.net.client.defaultReadTimeout");
}
// gh-19474
@Test
public void getConnectTimeoutWhenPropertyNotSetThenDefaultsToThirtySeconds() {
System.clearProperty("sun.net.client.defaultConnectTimeout");
assertThat(JwtDecoderProviderConfigurationUtils.getConnectTimeout()).isEqualTo(30000);
}
// gh-19474
@Test
public void getConnectTimeoutWhenPropertySetThenUses() {
System.setProperty("sun.net.client.defaultConnectTimeout", "5000");
assertThat(JwtDecoderProviderConfigurationUtils.getConnectTimeout()).isEqualTo(5000);
}
// gh-19474
@Test
public void getReadTimeoutWhenPropertyNotSetThenDefaultsToThirtySeconds() {
System.clearProperty("sun.net.client.defaultReadTimeout");
assertThat(JwtDecoderProviderConfigurationUtils.getReadTimeout()).isEqualTo(30000);
}
// gh-19474
@Test
public void getReadTimeoutWhenPropertySetThenUses() {
System.setProperty("sun.net.client.defaultReadTimeout", "5000");
assertThat(JwtDecoderProviderConfigurationUtils.getReadTimeout()).isEqualTo(5000);
}
@Test
public void getSignatureAlgorithmsWhenJwkSetSpecifiesAlgorithmThenUses() throws Exception {
JWKSource<SecurityContext> jwkSource = mock(JWKSource.class);
@@ -67,14 +67,17 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.TestKeys;
import org.springframework.security.oauth2.jose.jws.MacAlgorithm;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -391,6 +394,24 @@ public class NimbusJwtDecoderTests {
// @formatter:on
}
// gh-19474
@Test
public void withJwkSetUriWhenDefaultRestOperationsThenUsesConfiguredTimeouts() {
try {
System.setProperty("sun.net.client.defaultConnectTimeout", "12345");
System.setProperty("sun.net.client.defaultReadTimeout", "23456");
NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI);
RestOperations restOperations = (RestOperations) ReflectionTestUtils.getField(builder, "restOperations");
ClientHttpRequestFactory requestFactory = ((RestTemplate) restOperations).getRequestFactory();
assertThat(ReflectionTestUtils.getField(requestFactory, "connectTimeout")).isEqualTo(12345);
assertThat(ReflectionTestUtils.getField(requestFactory, "readTimeout")).isEqualTo(23456);
}
finally {
System.clearProperty("sun.net.client.defaultConnectTimeout");
System.clearProperty("sun.net.client.defaultReadTimeout");
}
}
@Test
public void cacheWhenNullThenThrowsException() {
NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(JWK_SET_URI);