diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java index 2bb49a2453b..8b16e6b2fd8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientProperties.java @@ -214,7 +214,8 @@ public class OAuth2ClientProperties { private String jwkSetUri; /** - * URI that an OpenID Connect Provider asserts as its Issuer Identifier. + * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 + * Authorization Server Metadata endpoint defined by RFC 8414. */ private String issuerUri; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java index 13feae1b360..481257a44a9 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapter.java @@ -80,7 +80,7 @@ public final class OAuth2ClientPropertiesRegistrationAdapter { Provider provider = providers.get(providerId); String issuer = provider.getIssuerUri(); if (issuer != null) { - Builder builder = ClientRegistrations.fromOidcIssuerLocation(issuer).registrationId(registrationId); + Builder builder = ClientRegistrations.fromIssuerLocation(issuer).registrationId(registrationId); return getBuilder(builder, provider); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java index 54d6c5665df..fe6eff975d7 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/OAuth2ResourceServerProperties.java @@ -82,7 +82,8 @@ public class OAuth2ResourceServerProperties { private String jwsAlgorithm = "RS256"; /** - * URI that an OpenID Connect Provider asserts as its Issuer Identifier. + * URI that can either be an OpenID Connect discovery endpoint or an OAuth 2.0 + * Authorization Server Metadata endpoint defined by RFC 8414. */ private String issuerUri; 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 356c0786217..a4f80a89254 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 @@ -79,7 +79,7 @@ class ReactiveOAuth2ResourceServerJwkConfiguration { @Bean @Conditional(IssuerUriCondition.class) ReactiveJwtDecoder jwtDecoderByIssuerUri() { - return ReactiveJwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); + return 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 c7ad97955fa..1bcb68eee8f 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 @@ -81,7 +81,7 @@ class OAuth2ResourceServerJwtConfiguration { @Bean @Conditional(IssuerUriCondition.class) JwtDecoder jwtDecoderByIssuerUri() { - return JwtDecoders.fromOidcIssuerLocation(this.properties.getIssuerUri()); + return JwtDecoders.fromIssuerLocation(this.properties.getIssuerUri()); } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java index 0bf8877d6e7..cda74fae725 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/client/OAuth2ClientPropertiesRegistrationAdapterTests.java @@ -48,6 +48,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; * @author Phillip Webb * @author Madhura Bhave * @author Thiago Hirata + * @author HaiTao Zhang */ class OAuth2ClientPropertiesRegistrationAdapterTests { @@ -209,7 +210,7 @@ class OAuth2ClientPropertiesRegistrationAdapterTests { Registration login = new OAuth2ClientProperties.Registration(); login.setClientId("clientId"); login.setClientSecret("clientSecret"); - testOidcConfiguration(login, "okta"); + testIssuerConfiguration(login, "okta", 0, 1); } @Test @@ -218,7 +219,23 @@ class OAuth2ClientPropertiesRegistrationAdapterTests { login.setProvider("okta-oidc"); login.setClientId("clientId"); login.setClientSecret("clientSecret"); - testOidcConfiguration(login, "okta-oidc"); + testIssuerConfiguration(login, "okta-oidc", 0, 1); + } + + @Test + void issuerUriConfigurationTriesOidcRfc8414UriSecond() throws Exception { + OAuth2ClientProperties.Registration login = new Registration(); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + testIssuerConfiguration(login, "okta", 1, 2); + } + + @Test + void issuerUriConfigurationTriesOAuthMetadataUriThird() throws Exception { + OAuth2ClientProperties.Registration login = new Registration(); + login.setClientId("clientId"); + login.setClientSecret("clientSecret"); + testIssuerConfiguration(login, "okta", 2, 3); } @Test @@ -273,12 +290,12 @@ class OAuth2ClientPropertiesRegistrationAdapterTests { return registration; } - private void testOidcConfiguration(OAuth2ClientProperties.Registration registration, String providerId) - throws Exception { + private void testIssuerConfiguration(OAuth2ClientProperties.Registration registration, String providerId, + int errorResponseCount, int numberOfRequests) throws Exception { this.server = new MockWebServer(); this.server.start(); String issuer = this.server.url("").toString(); - setupMockResponse(issuer); + setupMockResponsesWithErrors(issuer, errorResponseCount); OAuth2ClientProperties properties = new OAuth2ClientProperties(); Provider provider = new Provider(); provider.setIssuerUri(issuer); @@ -300,6 +317,7 @@ class OAuth2ClientPropertiesRegistrationAdapterTests { assertThat(userInfoEndpoint.getUri()).isEqualTo("https://example.com/oauth2/v3/userinfo"); assertThat(userInfoEndpoint.getAuthenticationMethod()) .isEqualTo(org.springframework.security.oauth2.core.AuthenticationMethod.HEADER); + assertThat(this.server.getRequestCount()).isEqualTo(numberOfRequests); } private void setupMockResponse(String issuer) throws JsonProcessingException { @@ -309,6 +327,14 @@ class OAuth2ClientPropertiesRegistrationAdapterTests { this.server.enqueue(mockResponse); } + private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException { + for (int i = 0; i < errorResponseCount; i++) { + MockResponse emptyResponse = new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value()); + this.server.enqueue(emptyResponse); + } + setupMockResponse(issuer); + } + private Map getResponse(String issuer) { Map response = new HashMap<>(); response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth"); 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 df8b13a82dc..b63e5eea3d3 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 @@ -64,6 +64,7 @@ import static org.mockito.Mockito.mock; * * @author Madhura Bhave * @author Artsiom Yudovin + * @author HaiTao Zhang */ class ReactiveOAuth2ResourceServerAutoConfigurationTests { @@ -94,14 +95,49 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws IOException { this.server = new MockWebServer(); this.server.start(); - String issuer = this.server.url("").toString(); + String path = "test"; + String issuer = this.server.url(path).toString(); String cleanIssuerPath = cleanIssuerPath(issuer); 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); + assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + }); + assertThat(this.server.getRequestCount()).isEqualTo(1); + } + + @Test + void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + String issuer = this.server.url("").toString(); + String cleanIssuerPath = cleanIssuerPath(issuer); + 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); + assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + }); + assertThat(this.server.getRequestCount()).isEqualTo(2); + } + + @Test + void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + String issuer = this.server.url("").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()).run((context) -> { assertThat(context).hasSingleBean(NimbusReactiveJwtDecoder.class); assertFilterConfiguredWithJwtAuthenticationManager(context); + assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); + assertThat(this.server.getRequestCount()).isEqualTo(3); } @Test @@ -322,6 +358,14 @@ class ReactiveOAuth2ResourceServerAutoConfigurationTests { this.server.enqueue(mockResponse); } + private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException { + for (int i = 0; i < errorResponseCount; i++) { + MockResponse emptyResponse = new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value()); + this.server.enqueue(emptyResponse); + } + setupMockResponse(issuer); + } + private Map getResponse(String issuer) { Map response = new HashMap<>(); response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth"); 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 aabbfb591d7..6ab7a67ac5e 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 @@ -58,6 +58,7 @@ import static org.mockito.Mockito.mock; * * @author Madhura Bhave * @author Artsiom Yudovin + * @author HaiTao Zhang */ class OAuth2ResourceServerAutoConfigurationTests { @@ -114,14 +115,48 @@ class OAuth2ResourceServerAutoConfigurationTests { void autoConfigurationShouldConfigureResourceServerUsingOidcIssuerUri() throws Exception { this.server = new MockWebServer(); this.server.start(); - String issuer = this.server.url("").toString(); + String path = "test"; + String issuer = this.server.url(path).toString(); String cleanIssuerPath = cleanIssuerPath(issuer); setupMockResponse(cleanIssuerPath); this.contextRunner.withPropertyValues("spring.security.oauth2.resourceserver.jwt.issuer-uri=http://" - + this.server.getHostName() + ":" + this.server.getPort()).run((context) -> { + + this.server.getHostName() + ":" + this.server.getPort() + "/" + path).run((context) -> { assertThat(context).hasSingleBean(JwtDecoder.class); - assertThat(getBearerTokenFilter(context)).isNotNull(); + assertThat(context.containsBean("jwtDecoderByIssuerUri")).isTrue(); + }); + assertThat(this.server.getRequestCount()).isEqualTo(1); + } + + @Test + void autoConfigurationShouldConfigureResourceServerUsingOidcRfc8414IssuerUri() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + String path = "test"; + String issuer = this.server.url(path).toString(); + String cleanIssuerPath = cleanIssuerPath(issuer); + 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.containsBean("jwtDecoderByIssuerUri")).isTrue(); }); + assertThat(this.server.getRequestCount()).isEqualTo(2); + } + + @Test + void autoConfigurationShouldConfigureResourceServerUsingOAuthIssuerUri() throws Exception { + this.server = new MockWebServer(); + this.server.start(); + String path = "test"; + 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.containsBean("jwtDecoderByIssuerUri")).isTrue(); + }); + assertThat(this.server.getRequestCount()).isEqualTo(3); } @Test @@ -306,6 +341,14 @@ class OAuth2ResourceServerAutoConfigurationTests { this.server.enqueue(mockResponse); } + private void setupMockResponsesWithErrors(String issuer, int errorResponseCount) throws JsonProcessingException { + for (int i = 0; i < errorResponseCount; i++) { + MockResponse emptyResponse = new MockResponse().setResponseCode(HttpStatus.NOT_FOUND.value()); + this.server.enqueue(emptyResponse); + } + setupMockResponse(issuer); + } + private Map getResponse(String issuer) { Map response = new HashMap<>(); response.put("authorization_endpoint", "https://example.com/o/oauth2/v2/auth");