From a03fe8befca2ee5eb0145a489150b3aa96f3c562 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 19 Apr 2023 11:35:04 +0100 Subject: [PATCH] Enable customization of JWK Set URI decoder builders Closes gh-20750 --- ...riReactiveJwtDecoderBuilderCustomizer.java | 40 +++++++++ ...eOAuth2ResourceServerJwkConfiguration.java | 21 +++-- .../JwkSetUriJwtDecoderBuilderCustomizer.java | 39 +++++++++ .../OAuth2ResourceServerJwtConfiguration.java | 18 +++-- ...2ResourceServerAutoConfigurationTests.java | 81 ++++++++++++++----- ...2ResourceServerAutoConfigurationTests.java | 30 +++++++ 6 files changed, 192 insertions(+), 37 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/JwkSetUriReactiveJwtDecoderBuilderCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/JwkSetUriReactiveJwtDecoderBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/JwkSetUriReactiveJwtDecoderBuilderCustomizer.java new file mode 100644 index 00000000000..8905e4e4942 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/JwkSetUriReactiveJwtDecoderBuilderCustomizer.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; + +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; + +/** + * Callback interface for the customization of the + * {@link JwkSetUriReactiveJwtDecoderBuilder} used to create the auto-configured + * {@link ReactiveJwtDecoder} for a JWK set URI that has been configured directly or + * obtained through an issuer URI. + * + * @author Andy Wilkinson + * @since 3.1.0 + */ +@FunctionalInterface +public interface JwkSetUriReactiveJwtDecoderBuilderCustomizer { + + /** + * Customize the given {@code builder}. + * @param builder the {@code builder} to customize + */ + void customize(JwkSetUriReactiveJwtDecoderBuilder builder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java index edaf54d1ea4..3b2546865b7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Set; import java.util.function.Supplier; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -45,8 +46,8 @@ import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtClaimValidator; import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder.JwkSetUriReactiveJwtDecoderBuilder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; -import org.springframework.security.oauth2.jwt.ReactiveJwtDecoders; import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.util.CollectionUtils; @@ -77,11 +78,12 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { @Bean @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") - ReactiveJwtDecoder jwtDecoder() { - NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = NimbusReactiveJwtDecoder + ReactiveJwtDecoder jwtDecoder(ObjectProvider customizers) { + JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder .withJwkSetUri(this.properties.getJwkSetUri()) - .jwsAlgorithms(this::jwsAlgorithms) - .build(); + .jwsAlgorithms(this::jwsAlgorithms); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusReactiveJwtDecoder nimbusReactiveJwtDecoder = builder.build(); String issuerUri = this.properties.getIssuerUri(); Supplier> defaultValidator = (issuerUri != null) ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault; @@ -138,10 +140,13 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { @Bean @Conditional(IssuerUriCondition.class) - SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() { + SupplierReactiveJwtDecoder jwtDecoderByIssuerUri( + ObjectProvider customizers) { return new SupplierReactiveJwtDecoder(() -> { - NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) ReactiveJwtDecoders - .fromIssuerLocation(this.properties.getIssuerUri()); + JwkSetUriReactiveJwtDecoderBuilder builder = NimbusReactiveJwtDecoder + .withIssuerLocation(this.properties.getIssuerUri()); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusReactiveJwtDecoder jwtDecoder = builder.build(); jwtDecoder.setJwtValidator( getValidators(() -> JwtValidators.createDefaultWithIssuer(this.properties.getIssuerUri()))); return jwtDecoder; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java new file mode 100644 index 00000000000..fd42e0a55b1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/JwkSetUriJwtDecoderBuilderCustomizer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.security.oauth2.resource.servlet; + +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; + +/** + * Callback interface for the customization of the {@link JwkSetUriJwtDecoderBuilder} used + * to create the auto-configured {@link JwtDecoder} for a JWK set URI that has been + * configured directly or obtained through an issuer URI. + * + * @author Andy Wilkinson + * @since 3.1.0 + */ +@FunctionalInterface +public interface JwkSetUriJwtDecoderBuilderCustomizer { + + /** + * Customize the given {@code builder}. + * @param builder the {@code builder} to customize + */ + void customize(JwkSetUriJwtDecoderBuilder builder); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java index 7b912d12950..5146570a28d 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerJwtConfiguration.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Set; import java.util.function.Supplier; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -44,9 +45,9 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtClaimNames; import org.springframework.security.oauth2.jwt.JwtClaimValidator; import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder.JwkSetUriJwtDecoderBuilder; import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.util.CollectionUtils; @@ -78,10 +79,11 @@ class OAuth2ResourceServerJwtConfiguration { @Bean @ConditionalOnProperty(name = "spring.security.oauth2.resourceserver.jwt.jwk-set-uri") - JwtDecoder jwtDecoderByJwkKeySetUri() { - NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) - .jwsAlgorithms(this::jwsAlgorithms) - .build(); + JwtDecoder jwtDecoderByJwkKeySetUri(ObjectProvider customizers) { + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withJwkSetUri(this.properties.getJwkSetUri()) + .jwsAlgorithms(this::jwsAlgorithms); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusJwtDecoder nimbusJwtDecoder = builder.build(); String issuerUri = this.properties.getIssuerUri(); Supplier> defaultValidator = (issuerUri != null) ? () -> JwtValidators.createDefaultWithIssuer(issuerUri) : JwtValidators::createDefault; @@ -138,10 +140,12 @@ class OAuth2ResourceServerJwtConfiguration { @Bean @Conditional(IssuerUriCondition.class) - SupplierJwtDecoder jwtDecoderByIssuerUri() { + SupplierJwtDecoder jwtDecoderByIssuerUri(ObjectProvider customizers) { return new SupplierJwtDecoder(() -> { String issuerUri = this.properties.getIssuerUri(); - NimbusJwtDecoder jwtDecoder = JwtDecoders.fromIssuerLocation(issuerUri); + JwkSetUriJwtDecoderBuilder builder = NimbusJwtDecoder.withIssuerLocation(issuerUri); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + NimbusJwtDecoder jwtDecoder = builder.build(); jwtDecoder.setJwtValidator(getValidators(() -> JwtValidators.createDefaultWithIssuer(issuerUri))); return jwtDecoder; }); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java index ac0068056b0..cc5a652d10c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -35,6 +35,7 @@ import okhttp3.mockwebserver.MockWebServer; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; @@ -43,6 +44,7 @@ import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplic import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -72,6 +74,8 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.web.server.WebFilter; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.springframework.security.config.Customizer.withDefaults; @@ -92,7 +96,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { private MockWebServer server; - private static final Duration TIMEOUT = Duration.ofSeconds(5); + private static final Duration TIMEOUT = Duration.ofSeconds(5000000); private static final String JWK_SET = "{\"keys\":[{\"kty\":\"RSA\",\"e\":\"AQAB\",\"use\":\"sig\"," + "\"kid\":\"one\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGm" @@ -127,9 +131,21 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { assertThat(nimbusReactiveJwtDecoder).extracting("jwtProcessor.arg$1.signatureAlgorithms") .asInstanceOf(InstanceOfAssertFactories.collection(SignatureAlgorithm.class)) .containsExactlyInAnyOrder(SignatureAlgorithm.RS512); + assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); }); } + private void assertJwkSetUriReactiveJwtDecoderBuilderCustomization( + AssertableReactiveWebApplicationContext context) { + JwkSetUriReactiveJwtDecoderBuilderCustomizer customizer = context.getBean("decoderBuilderCustomizer", + JwkSetUriReactiveJwtDecoderBuilderCustomizer.class); + JwkSetUriReactiveJwtDecoderBuilderCustomizer anotherCustomizer = context + .getBean("anotherDecoderBuilderCustomizer", JwkSetUriReactiveJwtDecoderBuilderCustomizer.class); + InOrder inOrder = inOrder(customizer, anotherCustomizer); + inOrder.verify(customizer).customize(any()); + inOrder.verify(anotherCustomizer).customize(any()); + } + @Test void autoConfigurationUsingJwkSetUriShouldConfigureResourceServerUsingMultipleJwsAlgorithms() { this.contextRunner @@ -141,6 +157,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { .asInstanceOf(InstanceOfAssertFactories.collection(SignatureAlgorithm.class)) .containsExactlyInAnyOrder(SignatureAlgorithm.RS256, SignatureAlgorithm.RS384, SignatureAlgorithm.RS512); + assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); }); } @@ -172,7 +189,6 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { } @Test - @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException { this.server = new MockWebServer(); this.server.start(); @@ -187,18 +203,32 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); assertFilterConfiguredWithJwtAuthenticationManager(context); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); - SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context - .getBean(SupplierReactiveJwtDecoder.class); - Mono reactiveJwtDecoderSupplier = (Mono) ReflectionTestUtils - .getField(supplierReactiveJwtDecoder, "jwtDecoderMono"); - reactiveJwtDecoderSupplier.block(TIMEOUT); + // Trigger calls to the issuer by decoding a token + decodeJwt(context); + assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); }); // The last request is to the JWK Set endpoint to look up the algorithm - assertThat(this.server.getRequestCount()).isOne(); + assertThat(this.server.getRequestCount()).isEqualTo(2); + } + + @SuppressWarnings("unchecked") + private void decodeJwt(AssertableReactiveWebApplicationContext context) { + SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context.getBean(SupplierReactiveJwtDecoder.class); + Mono reactiveJwtDecoderSupplier = (Mono) ReflectionTestUtils + .getField(supplierReactiveJwtDecoder, "jwtDecoderMono"); + try { + reactiveJwtDecoderSupplier.flatMap((decoder) -> decoder.decode("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0." + + "NHVaYe26MbtOYhSKkoKYdFVomg4i8ZJd8_-RU8VNbftc4TSMb4bXP3l3YlNWACwyXPGffz5aXHc6lty1Y2t4SWRqGteragsVdZufDn5BlnJl9pdR_kdVFUsra2rWKEofkZeIC4yWytE58sMIihvo9H1ScmmVwBcQP6XETqYd0aSHp1gOa9RdUPDvoXQ5oqygTqVtxaDr6wUFKrKItgBMzWIdNZ6y7O9E0DhEPTbE9rfBo6KTFsHAZnMg4k68CDp2woYIaXbmYTWcvbzIuHO7_37GT79XdIwkm95QJ7hYC9RiwrV7mesbY4PAahERJawntho0my942XheVLmGwLMBkQ")) + .block(TIMEOUT); + } + catch (Exception ex) { + // This fails, but it's enough to check that the expected HTTP calls + // are made + } } @Test - @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); @@ -212,18 +242,15 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); assertFilterConfiguredWithJwtAuthenticationManager(context); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); - SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context - .getBean(SupplierReactiveJwtDecoder.class); - Mono reactiveJwtDecoderSupplier = (Mono) ReflectionTestUtils - .getField(supplierReactiveJwtDecoder, "jwtDecoderMono"); - reactiveJwtDecoderSupplier.block(TIMEOUT); + // Trigger calls to the issuer by decoding a token + decodeJwt(context); + // assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); }); // The last request is to the JWK Set endpoint to look up the algorithm - assertThat(this.server.getRequestCount()).isEqualTo(2); + assertThat(this.server.getRequestCount()).isEqualTo(3); } @Test - @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); @@ -237,14 +264,12 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); assertFilterConfiguredWithJwtAuthenticationManager(context); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); - SupplierReactiveJwtDecoder supplierReactiveJwtDecoder = context - .getBean(SupplierReactiveJwtDecoder.class); - Mono reactiveJwtDecoderSupplier = (Mono) ReflectionTestUtils - .getField(supplierReactiveJwtDecoder, "jwtDecoderMono"); - reactiveJwtDecoderSupplier.block(TIMEOUT); + // Trigger calls to the issuer by decoding a token + decodeJwt(context); + assertJwkSetUriReactiveJwtDecoderBuilderCustomization(context); }); // The last request is to the JWK Set endpoint to look up the algorithm - assertThat(this.server.getRequestCount()).isEqualTo(3); + assertThat(this.server.getRequestCount()).isEqualTo(4); } @Test @@ -666,6 +691,18 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { return mock(MapReactiveUserDetailsService.class); } + @Bean + @Order(1) + JwkSetUriReactiveJwtDecoderBuilderCustomizer decoderBuilderCustomizer() { + return mock(JwkSetUriReactiveJwtDecoderBuilderCustomizer.class); + } + + @Bean + @Order(2) + JwkSetUriReactiveJwtDecoderBuilderCustomizer anotherDecoderBuilderCustomizer() { + return mock(JwkSetUriReactiveJwtDecoderBuilderCustomizer.class); + } + } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java index aeb1dee8d48..878415ec9f9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/servlet/OAuth2ResourceServerAutoConfigurationTests.java @@ -35,6 +35,7 @@ import okhttp3.mockwebserver.MockWebServer; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; @@ -43,6 +44,7 @@ import org.springframework.boot.test.context.assertj.AssertableWebApplicationCon import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -67,6 +69,8 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; /** @@ -105,9 +109,20 @@ class OAuth2ResourceServerAutoConfigurationTests { .run((context) -> { assertThat(context).hasSingleBean(JwtDecoder.class); assertThat(getBearerTokenFilter(context)).isNotNull(); + assertJwkSetUriJwtDecoderBuilderCustomization(context); }); } + private void assertJwkSetUriJwtDecoderBuilderCustomization(AssertableWebApplicationContext context) { + JwkSetUriJwtDecoderBuilderCustomizer customizer = context.getBean("decoderBuilderCustomizer", + JwkSetUriJwtDecoderBuilderCustomizer.class); + JwkSetUriJwtDecoderBuilderCustomizer anotherCustomizer = context.getBean("anotherDecoderBuilderCustomizer", + JwkSetUriJwtDecoderBuilderCustomizer.class); + InOrder inOrder = inOrder(customizer, anotherCustomizer); + inOrder.verify(customizer).customize(any()); + inOrder.verify(anotherCustomizer).customize(any()); + } + @Test void autoConfigurationShouldMatchDefaultJwsAlgorithm() { this.contextRunner @@ -194,6 +209,7 @@ class OAuth2ResourceServerAutoConfigurationTests { Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils .getField(supplierJwtDecoderBean, "delegate"); jwtDecoderSupplier.get(); + assertJwkSetUriJwtDecoderBuilderCustomization(context); }); // The last request is to the JWK Set endpoint to look up the algorithm assertThat(this.server.getRequestCount()).isEqualTo(2); @@ -218,6 +234,7 @@ class OAuth2ResourceServerAutoConfigurationTests { Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils .getField(supplierJwtDecoderBean, "delegate"); jwtDecoderSupplier.get(); + assertJwkSetUriJwtDecoderBuilderCustomization(context); }); // The last request is to the JWK Set endpoint to look up the algorithm assertThat(this.server.getRequestCount()).isEqualTo(3); @@ -243,6 +260,7 @@ class OAuth2ResourceServerAutoConfigurationTests { Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils .getField(supplierJwtDecoderBean, "delegate"); jwtDecoderSupplier.get(); + assertJwkSetUriJwtDecoderBuilderCustomization(context); }); // The last request is to the JWK Set endpoint to look up the algorithm assertThat(this.server.getRequestCount()).isEqualTo(4); @@ -678,6 +696,18 @@ class OAuth2ResourceServerAutoConfigurationTests { @EnableWebSecurity static class TestConfig { + @Bean + @Order(1) + JwkSetUriJwtDecoderBuilderCustomizer decoderBuilderCustomizer() { + return mock(JwkSetUriJwtDecoderBuilderCustomizer.class); + } + + @Bean + @Order(2) + JwkSetUriJwtDecoderBuilderCustomizer anotherDecoderBuilderCustomizer() { + return mock(JwkSetUriJwtDecoderBuilderCustomizer.class); + } + } @Configuration(proxyBeanMethods = false)