From 4e9f536a8e054c2a6b317ed3f75a29596d90c5f6 Mon Sep 17 00:00:00 2001 From: Anjee Date: Fri, 1 Oct 2021 13:22:37 -0700 Subject: [PATCH] Auto-configure JwtSupplierDecoder to defer OIDC lookup See gh-28169 --- ...eOAuth2ResourceServerJwkConfiguration.java | 6 ++-- .../OAuth2ResourceServerJwtConfiguration.java | 5 +-- ...2ResourceServerAutoConfigurationTests.java | 34 +++++++++++++++---- ...2ResourceServerAutoConfigurationTests.java | 24 +++++++++++-- .../spring-boot-dependencies/build.gradle | 2 +- 5 files changed, 56 insertions(+), 15 deletions(-) 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 03c7a34bc31..3f937ac536c 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 @@ -37,6 +37,7 @@ import org.springframework.security.oauth2.jwt.JwtValidators; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; 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; /** @@ -91,8 +92,9 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { @Bean @Conditional(IssuerUriCondition.class) - ReactiveJwtDecoder jwtDecoderByIssuerUri() { - return ReactiveJwtDecoders.fromIssuerLocation(this.properties.getIssuerUri()); + SupplierReactiveJwtDecoder jwtDecoderByIssuerUri() { + return new SupplierReactiveJwtDecoder( + () -> ReactiveJwtDecoders.fromIssuerLocation(this.properties.getIssuerUri())); } } 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 8706d09d733..b9ca979124c 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 @@ -39,6 +39,7 @@ 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.SupplierJwtDecoder; import org.springframework.security.web.SecurityFilterChain; /** @@ -91,8 +92,8 @@ class OAuth2ResourceServerJwtConfiguration { @Bean @Conditional(IssuerUriCondition.class) - JwtDecoder jwtDecoderByIssuerUri() { - return JwtDecoders.fromIssuerLocation(this.properties.getIssuerUri()); + SupplierJwtDecoder jwtDecoderByIssuerUri() { + return new SupplierJwtDecoder(() -> JwtDecoders.fromIssuerLocation(this.properties.getIssuerUri())); } } 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 4fc7b29b821..fb4f3880cf6 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 @@ -31,6 +31,7 @@ import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.FilteredClassLoader; @@ -52,6 +53,7 @@ import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.SupplierReactiveJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; import org.springframework.security.oauth2.server.resource.authentication.OpaqueTokenReactiveAuthenticationManager; @@ -129,6 +131,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { } @Test + @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException { this.server = new MockWebServer(); this.server.start(); @@ -138,15 +141,21 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { setupMockResponse(cleanIssuerPath); this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path).run((context) -> { - assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); + 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"); + ReactiveJwtDecoder reactiveJwtDecoder = reactiveJwtDecoderSupplier.block(); }); // 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(1); } @Test + @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); @@ -155,15 +164,21 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { setupMockResponsesWithErrors(cleanIssuerPath, 1); this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + this.server.getPort()).run((context) -> { - assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); + 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"); + ReactiveJwtDecoder reactiveJwtDecoder = reactiveJwtDecoderSupplier.block(); }); // 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(2); } @Test + @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); @@ -172,12 +187,17 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { setupMockResponsesWithErrors(cleanIssuerPath, 2); this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + this.server.getPort()).run((context) -> { - assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); + 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"); + ReactiveJwtDecoder reactiveJwtDecoder = reactiveJwtDecoderSupplier.block(); }); // The last request is to the JWK Set endpoint to look up the algorithm - assertThat(this.server.getRequestCount()).isEqualTo(4); + assertThat(this.server.getRequestCount()).isEqualTo(3); } @Test @@ -228,7 +248,7 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { + this.server.getPort(), "spring.security.oauth2.resourceserver.jwt.public-key-location=classpath:public-key-location") .run((context) -> { - assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); + assertThat(context).hasSingleBean(SupplierReactiveJwtDecoder.class); assertFilterConfiguredWithJwtAuthenticationManager(context); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); 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 288832babfb..e0a2beb5032 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 @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Supplier; import javax.servlet.Filter; @@ -49,6 +50,7 @@ import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtIssuerValidator; +import org.springframework.security.oauth2.jwt.SupplierJwtDecoder; import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider; import org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector; @@ -127,6 +129,7 @@ class OAuth2ResourceServerAutoConfigurationTests { } @Test + @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); @@ -136,14 +139,19 @@ class OAuth2ResourceServerAutoConfigurationTests { setupMockResponse(cleanIssuerPath); this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path).run((context) -> { - assertThat(context).hasSingleBean(JwtDecoder.class); + assertThat(context).hasSingleBean(SupplierJwtDecoder.class); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class); + Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils + .getField(supplierJwtDecoderBean, "jwtDecoderSupplier"); + JwtDecoder jwtDecoder = jwtDecoderSupplier.get(); }); // The last request is to the JWK Set endpoint to look up the algorithm assertThat(this.server.getRequestCount()).isEqualTo(2); } @Test + @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); @@ -153,14 +161,19 @@ class OAuth2ResourceServerAutoConfigurationTests { setupMockResponsesWithErrors(cleanIssuerPath, 1); this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path).run((context) -> { - assertThat(context).hasSingleBean(JwtDecoder.class); + assertThat(context).hasSingleBean(SupplierJwtDecoder.class); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class); + Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils + .getField(supplierJwtDecoderBean, "jwtDecoderSupplier"); + JwtDecoder jwtDecoder = jwtDecoderSupplier.get(); }); // The last request is to the JWK Set endpoint to look up the algorithm assertThat(this.server.getRequestCount()).isEqualTo(3); } @Test + @SuppressWarnings("unchecked") void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); @@ -168,10 +181,15 @@ class OAuth2ResourceServerAutoConfigurationTests { String issuer = this.server.url(path).toString(); String cleanIssuerPath = cleanIssuerPath(issuer); setupMockResponsesWithErrors(cleanIssuerPath, 2); + this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" + this.server.getHostName() + ":" + this.server.getPort() + "/" + path).run((context) -> { - assertThat(context).hasSingleBean(JwtDecoder.class); + assertThat(context).hasSingleBean(SupplierJwtDecoder.class); assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + SupplierJwtDecoder supplierJwtDecoderBean = context.getBean(SupplierJwtDecoder.class); + Supplier jwtDecoderSupplier = (Supplier) ReflectionTestUtils + .getField(supplierJwtDecoderBean, "jwtDecoderSupplier"); + JwtDecoder jwtDecoder = jwtDecoderSupplier.get(); }); // The last request is to the JWK Set endpoint to look up the algorithm assertThat(this.server.getRequestCount()).isEqualTo(4); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 4c666f34d57..cb4724831a2 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -1693,7 +1693,7 @@ bom { ] } } - library("Spring Security", "5.6.0-M3") { + library("Spring Security", "5.6.0-SNAPSHOT") { group("org.springframework.security") { imports = [ "spring-security-bom"