Skip to content

Commit b196db5

Browse files
authored
Apply JWT decoder timeout properties for AAD and B2C resource servers (#49329)
1 parent 34633ac commit b196db5

7 files changed

Lines changed: 179 additions & 29 deletions

File tree

sdk/spring/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module.
1212
#### Bugs Fixed
1313

1414
- Fixed Redis Lettuce passwordless autoconfiguration so a user-defined `LettuceClientConfigurationBuilderCustomizer` no longer suppresses the Azure customizer bean that configures Azure Redis credentials and RESP2 support.
15+
- Applied `jwt-connect-timeout` and `jwt-read-timeout` properties to the RestTemplate used by the JWT decoder in AAD and B2C resource server configurations, preventing indefinite hanging when fetching JWK keys ([#49329](https://github.com/Azure/azure-sdk-for-java/pull/49329)).
1516

1617
#### Other Changes
1718

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,10 @@ JwtDecoder jwtDecoder(AadAuthenticationProperties aadAuthenticationProperties) {
5757
aadAuthenticationProperties.getProfile().getEnvironment().getActiveDirectoryEndpoint(), tenantId);
5858
NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder
5959
.withJwkSetUri(identityEndpoints.getJwkSetEndpoint())
60-
.restOperations(createRestTemplate(restTemplateBuilder))
61-
.build();
60+
.restOperations(createRestTemplate(restTemplateBuilder
61+
.connectTimeout(aadAuthenticationProperties.getJwtConnectTimeout())
62+
.readTimeout(aadAuthenticationProperties.getJwtReadTimeout())))
63+
.build();
6264
List<OAuth2TokenValidator<Jwt>> validators = createDefaultValidator(aadAuthenticationProperties);
6365
nimbusJwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(validators));
6466
return nimbusJwtDecoder;

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/properties/AadAuthenticationProperties.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
package com.azure.spring.cloud.autoconfigure.implementation.aad.configuration.properties;
55

66
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.properties.AuthorizationClientProperties;
7-
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
7+
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
1010
import org.springframework.beans.factory.InitializingBean;
@@ -90,25 +90,22 @@ public class AadAuthenticationProperties implements InitializingBean {
9090
private final Map<String, Object> authenticateAdditionalParameters = new HashMap<>();
9191

9292
/**
93-
* Connection Timeout (duration) for the JWKSet Remote URL call. The default value is `500s`.
94-
* @deprecated If you want to configure this, please provide a 'RestOperations' bean.
93+
* Connection Timeout (duration) for the JWKSet Remote URL call.
94+
* The default value is {@value com.nimbusds.jose.jwk.source.JWKSourceBuilder#DEFAULT_HTTP_CONNECT_TIMEOUT} milliseconds.
9595
*/
96-
@Deprecated
97-
private Duration jwtConnectTimeout = Duration.ofMillis(RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT);
96+
private Duration jwtConnectTimeout = Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT);
9897

9998
/**
100-
* Read Timeout (duration) for the JWKSet Remote URL call. The default value is `500s`.
101-
* @deprecated If you want to configure this, please provide a 'RestOperations' bean.
99+
* Read Timeout (duration) for the JWKSet Remote URL call.
100+
* The default value is {@value com.nimbusds.jose.jwk.source.JWKSourceBuilder#DEFAULT_HTTP_READ_TIMEOUT} milliseconds.
102101
*/
103-
@Deprecated
104-
private Duration jwtReadTimeout = Duration.ofMillis(RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT);
102+
private Duration jwtReadTimeout = Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT);
105103

106104
/**
107-
* Size limit in Bytes of the JWKSet Remote URL call. The default value is `51200`.
108-
* @deprecated If you want to configure this, please provide a 'RestOperations' bean.
105+
* Size limit in Bytes of the JWKSet Remote URL call.
106+
* The default value is {@value com.nimbusds.jose.jwk.source.JWKSourceBuilder#DEFAULT_HTTP_SIZE_LIMIT} bytes.
109107
*/
110-
@Deprecated
111-
private int jwtSizeLimit = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */
108+
private int jwtSizeLimit = JWKSourceBuilder.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */
112109

113110
/**
114111
* The lifespan (duration) of the cached JWK set before it expires.

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aadb2c/configuration/AadB2cResourceServerAutoConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ public class AadB2cResourceServerAutoConfiguration {
5353

5454
AadB2cResourceServerAutoConfiguration(AadB2cProperties properties, RestTemplateBuilder restTemplateBuilder) {
5555
this.properties = properties;
56-
this.restTemplateBuilder = restTemplateBuilder;
56+
this.restTemplateBuilder = restTemplateBuilder
57+
.connectTimeout(properties.getJwtConnectTimeout())
58+
.readTimeout(properties.getJwtReadTimeout());
5759
}
5860

5961
@Bean

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/aadb2c/configuration/properties/AadB2cProperties.java

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
package com.azure.spring.cloud.autoconfigure.implementation.aadb2c.configuration.properties;
44

55
import com.azure.spring.cloud.autoconfigure.implementation.aadb2c.security.exception.AadB2cConfigurationException;
6-
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
6+
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
77
import org.springframework.beans.factory.InitializingBean;
88
import org.springframework.boot.context.properties.NestedConfigurationProperty;
99
import org.springframework.util.CollectionUtils;
@@ -60,25 +60,22 @@ public class AadB2cProperties implements InitializingBean {
6060
private String appIdUri;
6161

6262
/**
63-
* Connection Timeout(duration) for the JWKSet Remote URL call. The default value is `500s`.
64-
* @deprecated If you want to configure this, please provide a RestOperations bean.
63+
* Connection Timeout (duration) for the JWKSet Remote URL call.
64+
* The default value is {@value com.nimbusds.jose.jwk.source.JWKSourceBuilder#DEFAULT_HTTP_CONNECT_TIMEOUT} milliseconds.
6565
*/
66-
@Deprecated
67-
private Duration jwtConnectTimeout = Duration.ofMillis(RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT);
66+
private Duration jwtConnectTimeout = Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT);
6867

6968
/**
70-
* Read Timeout(duration) for the JWKSet Remote URL call. The default value is `500s`.
71-
* @deprecated If you want to configure this, please provide a RestOperations bean.
69+
* Read Timeout (duration) for the JWKSet Remote URL call.
70+
* The default value is {@value com.nimbusds.jose.jwk.source.JWKSourceBuilder#DEFAULT_HTTP_READ_TIMEOUT} milliseconds.
7271
*/
73-
@Deprecated
74-
private Duration jwtReadTimeout = Duration.ofMillis(RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT);
72+
private Duration jwtReadTimeout = Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT);
7573

7674
/**
77-
* Size limit in Bytes of the JWKSet Remote URL call. The default value is `50*1024`.
78-
* @deprecated If you want to configure this, please provide a RestOperations bean.
75+
* Size limit in Bytes of the JWKSet Remote URL call.
76+
* The default value is {@value com.nimbusds.jose.jwk.source.JWKSourceBuilder#DEFAULT_HTTP_SIZE_LIMIT} bytes.
7977
*/
80-
@Deprecated
81-
private int jwtSizeLimit = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */
78+
private int jwtSizeLimit = JWKSourceBuilder.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */
8279

8380
/**
8481
* Redirect URL after logout.

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aad/configuration/AadResourceServerConfigurationTests.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.jwt.AadJwtIssuerValidator;
88
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.AadResourceServerHttpSecurityConfigurer;
99
import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration;
10+
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
1011
import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector;
1112
import org.junit.jupiter.api.Test;
1213
import org.springframework.boot.autoconfigure.AutoConfigurations;
@@ -32,6 +33,7 @@
3233
import org.springframework.security.web.SecurityFilterChain;
3334
import org.springframework.test.util.ReflectionTestUtils;
3435

36+
import java.time.Duration;
3537
import java.util.Collection;
3638
import java.util.LinkedHashMap;
3739
import java.util.List;
@@ -67,6 +69,44 @@ void testCreateJwtDecoderByJwkKeySetUri() {
6769
});
6870
}
6971

72+
@Test
73+
void testJwtDecoderTimeoutDefaultValues() {
74+
resourceServerContextRunner()
75+
.withPropertyValues("spring.cloud.azure.active-directory.enabled=true")
76+
.run(context -> {
77+
AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class);
78+
assertThat(properties.getJwtConnectTimeout())
79+
.isEqualTo(Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT));
80+
assertThat(properties.getJwtReadTimeout())
81+
.isEqualTo(Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT));
82+
// Verify the default timeouts are applied to the RestTemplate used by the JwtDecoder
83+
final JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
84+
verifyJwtDecoderRestTemplateTimeouts(jwtDecoder,
85+
JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT,
86+
JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT);
87+
});
88+
}
89+
90+
@Test
91+
void testJwtDecoderTimeoutCustomValues() {
92+
resourceServerContextRunner()
93+
.withPropertyValues(
94+
"spring.cloud.azure.active-directory.enabled=true",
95+
"spring.cloud.azure.active-directory.jwt-connect-timeout=2000",
96+
"spring.cloud.azure.active-directory.jwt-read-timeout=3000")
97+
.run(context -> {
98+
AadAuthenticationProperties properties = context.getBean(AadAuthenticationProperties.class);
99+
assertThat(properties.getJwtConnectTimeout()).isEqualTo(Duration.ofMillis(2000));
100+
assertThat(properties.getJwtReadTimeout()).isEqualTo(Duration.ofMillis(3000));
101+
// Verify JwtDecoder is still created successfully with custom timeouts
102+
final JwtDecoder jwtDecoder = context.getBean(JwtDecoder.class);
103+
assertThat(jwtDecoder).isNotNull();
104+
assertThat(jwtDecoder).isExactlyInstanceOf(NimbusJwtDecoder.class);
105+
// Verify the configured timeouts are applied to the RestTemplate used by the JwtDecoder
106+
verifyJwtDecoderRestTemplateTimeouts(jwtDecoder, 2000, 3000);
107+
});
108+
}
109+
70110
@Test
71111
void testNotAudienceDefaultValidator() {
72112
resourceServerRunner()
@@ -364,4 +404,52 @@ public Collection<GrantedAuthority> convert(Jwt source) {
364404
return null;
365405
}
366406
}
407+
408+
/**
409+
* Verifies that the RestTemplate used by the NimbusJwtDecoder for JWK retrieval
410+
* has the expected connect and read timeouts applied to its ClientHttpRequestFactory.
411+
*/
412+
@SuppressWarnings("unchecked")
413+
private static void verifyJwtDecoderRestTemplateTimeouts(JwtDecoder jwtDecoder,
414+
int expectedConnectTimeoutMs,
415+
int expectedReadTimeoutMs) {
416+
// NimbusJwtDecoder -> jwtProcessor (DefaultJWTProcessor)
417+
Object jwtProcessor = ReflectionTestUtils.getField(jwtDecoder, "jwtProcessor");
418+
assertThat(jwtProcessor).isInstanceOf(com.nimbusds.jwt.proc.DefaultJWTProcessor.class);
419+
420+
// DefaultJWTProcessor -> JWSKeySelector (JWSVerificationKeySelector)
421+
com.nimbusds.jose.proc.JWSKeySelector<?> keySelector =
422+
((com.nimbusds.jwt.proc.DefaultJWTProcessor<?>) jwtProcessor).getJWSKeySelector();
423+
assertThat(keySelector).isInstanceOf(com.nimbusds.jose.proc.JWSVerificationKeySelector.class);
424+
425+
// JWSVerificationKeySelector -> JWKSource (JWKSetBasedJWKSource)
426+
com.nimbusds.jose.jwk.source.JWKSource<?> jwkSource =
427+
((com.nimbusds.jose.proc.JWSVerificationKeySelector<?>) keySelector).getJWKSource();
428+
assertThat(jwkSource).isInstanceOf(com.nimbusds.jose.jwk.source.JWKSetBasedJWKSource.class);
429+
430+
// JWKSetBasedJWKSource -> JWKSetSource (CachingJWKSetSource -> JWKSetSourceWrapper -> actual source)
431+
Object jwkSetSource =
432+
((com.nimbusds.jose.jwk.source.JWKSetBasedJWKSource<?>) jwkSource).getJWKSetSource();
433+
434+
// Unwrap JWKSetSourceWrapper chain to find the source with restOperations
435+
while (jwkSetSource instanceof com.nimbusds.jose.jwk.source.JWKSetSourceWrapper<?> wrapper) {
436+
jwkSetSource = wrapper.getSource();
437+
}
438+
439+
// actual source -> restOperations (RestTemplate)
440+
Object restOperations = ReflectionTestUtils.getField(jwkSetSource, "restOperations");
441+
assertThat(restOperations).isInstanceOf(org.springframework.web.client.RestTemplate.class);
442+
443+
// RestTemplate -> ClientHttpRequestFactory
444+
org.springframework.http.client.ClientHttpRequestFactory requestFactory =
445+
((org.springframework.web.client.RestTemplate) restOperations).getRequestFactory();
446+
447+
// Verify timeouts on the request factory (may be stored as Duration or int)
448+
Object connectTimeoutValue = ReflectionTestUtils.getField(requestFactory, "connectTimeout");
449+
Object readTimeoutValue = ReflectionTestUtils.getField(requestFactory, "readTimeout");
450+
int connectTimeout = connectTimeoutValue instanceof java.time.Duration d ? (int) d.toMillis() : (int) connectTimeoutValue;
451+
int readTimeout = readTimeoutValue instanceof java.time.Duration d ? (int) d.toMillis() : (int) readTimeoutValue;
452+
assertThat(connectTimeout).isEqualTo(expectedConnectTimeoutMs);
453+
assertThat(readTimeout).isEqualTo(expectedReadTimeoutMs);
454+
}
367455
}

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/aadb2c/configuration/AadB2cResourceServerAutoConfigurationTests.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.azure.spring.cloud.autoconfigure.implementation.aadb2c.configuration.properties.AadB2cProperties;
1010
import com.azure.spring.cloud.autoconfigure.implementation.aadb2c.security.jwt.AadB2cTrustedIssuerRepository;
1111
import com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalPropertiesAutoConfiguration;
12+
import com.nimbusds.jose.jwk.source.JWKSourceBuilder;
1213
import com.nimbusds.jose.proc.SecurityContext;
1314
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
1415
import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector;
@@ -29,6 +30,7 @@
2930
import org.springframework.security.oauth2.jwt.JwtDecoder;
3031
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
3132

33+
import java.time.Duration;
3234
import java.util.Set;
3335

3436
import static org.assertj.core.api.Assertions.assertThat;
@@ -141,6 +143,36 @@ void testB2COnlyResourceServerBean() {
141143
getResourceServerContextRunner().run(b2CResourceServerBean());
142144
}
143145

146+
@Test
147+
void testB2CTimeoutDefaultValues() {
148+
getResourceServerContextRunner().run(context -> {
149+
AadB2cProperties properties = context.getBean(AadB2cProperties.class);
150+
assertThat(properties.getJwtConnectTimeout())
151+
.isEqualTo(Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT));
152+
assertThat(properties.getJwtReadTimeout())
153+
.isEqualTo(Duration.ofMillis(JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT));
154+
// Verify the default timeouts are applied to the RestTemplate used by the ResourceRetriever
155+
verifyResourceRetrieverRestTemplateTimeouts(context,
156+
JWKSourceBuilder.DEFAULT_HTTP_CONNECT_TIMEOUT,
157+
JWKSourceBuilder.DEFAULT_HTTP_READ_TIMEOUT);
158+
});
159+
}
160+
161+
@Test
162+
void testB2CTimeoutCustomValues() {
163+
getResourceServerContextRunner()
164+
.withPropertyValues(
165+
"spring.cloud.azure.active-directory.b2c.jwt-connect-timeout=2000",
166+
"spring.cloud.azure.active-directory.b2c.jwt-read-timeout=3000")
167+
.run(context -> {
168+
AadB2cProperties properties = context.getBean(AadB2cProperties.class);
169+
assertThat(properties.getJwtConnectTimeout()).isEqualTo(Duration.ofMillis(2000));
170+
assertThat(properties.getJwtReadTimeout()).isEqualTo(Duration.ofMillis(3000));
171+
// Verify the custom timeouts are applied to the RestTemplate used by the ResourceRetriever
172+
verifyResourceRetrieverRestTemplateTimeouts(context, 2000, 3000);
173+
});
174+
}
175+
144176
@Test
145177
void testResourceServerConditionsIsInvokedWhenAADB2CEnableFileExists() {
146178
try (MockedStatic<BeanUtils> beanUtils = mockStatic(BeanUtils.class, Mockito.CALLS_REAL_METHODS)) {
@@ -301,4 +333,35 @@ void testExistJWTClaimsSetAwareJWSKeySelectorBean() {
301333
assertThat(jwsKeySelector).isExactlyInstanceOf(AadIssuerJwsKeySelector.class);
302334
});
303335
}
336+
337+
/**
338+
* Verifies that the RestTemplate used by the ResourceRetriever for JWK retrieval
339+
* has the expected connect and read timeouts applied to its ClientHttpRequestFactory.
340+
*/
341+
private static void verifyResourceRetrieverRestTemplateTimeouts(ApplicationContext context,
342+
int expectedConnectTimeoutMs,
343+
int expectedReadTimeoutMs) {
344+
com.nimbusds.jose.util.ResourceRetriever resourceRetriever =
345+
context.getBean(com.nimbusds.jose.util.ResourceRetriever.class);
346+
assertThat(resourceRetriever).isNotNull();
347+
348+
// RestOperationsResourceRetriever -> restOperations (RestTemplate)
349+
Object restOperations = org.springframework.test.util.ReflectionTestUtils
350+
.getField(resourceRetriever, "restOperations");
351+
assertThat(restOperations).isInstanceOf(org.springframework.web.client.RestTemplate.class);
352+
353+
// RestTemplate -> ClientHttpRequestFactory
354+
org.springframework.http.client.ClientHttpRequestFactory requestFactory =
355+
((org.springframework.web.client.RestTemplate) restOperations).getRequestFactory();
356+
357+
// Verify timeouts on the request factory (may be stored as Duration or int)
358+
Object connectTimeoutValue = org.springframework.test.util.ReflectionTestUtils
359+
.getField(requestFactory, "connectTimeout");
360+
Object readTimeoutValue = org.springframework.test.util.ReflectionTestUtils
361+
.getField(requestFactory, "readTimeout");
362+
int connectTimeout = connectTimeoutValue instanceof java.time.Duration d ? (int) d.toMillis() : (int) connectTimeoutValue;
363+
int readTimeout = readTimeoutValue instanceof java.time.Duration d ? (int) d.toMillis() : (int) readTimeoutValue;
364+
assertThat(connectTimeout).isEqualTo(expectedConnectTimeoutMs);
365+
assertThat(readTimeout).isEqualTo(expectedReadTimeoutMs);
366+
}
304367
}

0 commit comments

Comments
 (0)