diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java new file mode 100644 index 00000000000..72cfb654de1 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfiguration.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Reactive OAuth2 resource server + * support. + * + * @author Madhura Bhave + * @since 2.1.0 + */ +@Configuration +@AutoConfigureBefore(ReactiveSecurityAutoConfiguration.class) +@EnableConfigurationProperties(OAuth2ResourceServerProperties.class) +@ConditionalOnClass({ EnableWebFluxSecurity.class, BearerTokenAuthenticationToken.class }) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@Import({ ReactiveOAuth2ResourceServerJwkConfiguration.class, + ReactiveOAuth2ResourceServerWebSecurityConfiguration.class }) +public class ReactiveOAuth2ResourceServerAutoConfiguration { + +} 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 new file mode 100644 index 00000000000..e9977b27c70 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerJwkConfiguration.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; + +/** + * Configures a {@link ReactiveJwtDecoder} when a JWK Set URI is available. + * + * @author Madhura Bhave + */ +@Configuration +class ReactiveOAuth2ResourceServerJwkConfiguration { + + private final OAuth2ResourceServerProperties properties; + + ReactiveOAuth2ResourceServerJwkConfiguration( + OAuth2ResourceServerProperties properties) { + this.properties = properties; + } + + @Bean + @ConditionalOnProperty(name = "spring.security.oauth2.resource.jwt.jwk.set-uri") + @ConditionalOnMissingBean + public ReactiveJwtDecoder jwtDecoder() { + return new NimbusReactiveJwtDecoder( + this.properties.getJwt().getJwk().getSetUri()); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerWebSecurityConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerWebSecurityConfiguration.java new file mode 100644 index 00000000000..c481fdeabfe --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerWebSecurityConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2018 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 + * + * http://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.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * Configures a {@link SecurityWebFilterChain} for Reactive OAuth2 resource server support + * if a {@link ReactiveJwtDecoder} bean is present. + * + * @author Madhura Bhave + */ +@Configuration +@ConditionalOnBean(ReactiveJwtDecoder.class) +class ReactiveOAuth2ResourceServerWebSecurityConfiguration { + + private final ReactiveJwtDecoder jwtDecoder; + + ReactiveOAuth2ResourceServerWebSecurityConfiguration(ReactiveJwtDecoder jwtDecoder) { + this.jwtDecoder = jwtDecoder; + } + + @Bean + @ConditionalOnMissingBean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http.authorizeExchange().anyExchange().authenticated().and().oauth2() + .resourceServer().jwt().jwtDecoder(this.jwtDecoder); + return http.build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/package-info.java new file mode 100644 index 00000000000..586542b4160 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2018 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 + * + * http://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. + */ + +/** + * Auto-configuration for Spring Security's Reactive OAuth2 resource server. + */ +package org.springframework.boot.autoconfigure.security.oauth2.resource.reactive; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index c8dd4ff1da8..023f9d6b4d2 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -106,6 +106,7 @@ org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\ org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\ org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\ org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\ +org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\ org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\ org.springframework.boot.autoconfigure.task.TaskExecutorAutoConfiguration,\ org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\ 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 new file mode 100644 index 00000000000..fd0095a4454 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/oauth2/resource/reactive/ReactiveOAuth2ResourceServerAutoConfigurationTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2012-2018 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 + * + * http://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 java.util.List; + +import org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext; +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.BeanIds; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder; +import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder; +import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken; +import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager; +import org.springframework.security.web.server.MatcherSecurityWebFilterChain; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.server.WebFilter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ReactiveOAuth2ResourceServerAutoConfiguration}. + * + * @author Madhura Bhave + */ +public class ReactiveOAuth2ResourceServerAutoConfigurationTests { + + private ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations + .of(ReactiveOAuth2ResourceServerAutoConfiguration.class)) + .withUserConfiguration(TestConfig.class); + + @Test + public void autoConfigurationShouldConfigureResourceServer() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resource.jwt.jwk.set-uri=http://jwk-set-uri.com") + .run((context) -> { + assertThat(context.getBean(ReactiveJwtDecoder.class)) + .isInstanceOf(NimbusReactiveJwtDecoder.class); + assertFilterConfiguredWithJwtAuthenticationManager(context); + }); + } + + @Test + public void autoConfigurationWhenJwkSetUriNullShouldNotFail() { + this.contextRunner.run((context) -> assertThat(context) + .doesNotHaveBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN)); + } + + @Test + public void jwtDecoderBeanIsConditionalOnMissingBean() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resource.jwt.jwk.set-uri=http://jwk-set-uri.com") + .withUserConfiguration(JwtDecoderConfig.class) + .run((this::assertFilterConfiguredWithJwtAuthenticationManager)); + } + + @Test + public void autoConfigurationShouldBeConditionalOnBearerTokenAuthenticationTokenClass() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resource.jwt.jwk.set-uri=http://jwk-set-uri.com") + .withUserConfiguration(JwtDecoderConfig.class) + .withClassLoader( + new FilteredClassLoader(BearerTokenAuthenticationToken.class)) + .run((context) -> assertThat(context) + .doesNotHaveBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN)); + } + + @Test + public void autoConfigurationWhenSecurityWebFilterChainConfigPresentShouldNotAddOne() { + this.contextRunner.withPropertyValues( + "spring.security.oauth2.resource.jwt.jwk.set-uri=http://jwk-set-uri.com") + .withUserConfiguration(SecurityWebFilterChainConfig.class) + .run((context) -> { + assertThat(context).hasSingleBean(SecurityWebFilterChain.class); + assertThat(context).hasBean("testSpringSecurityFilterChain"); + }); + } + + @SuppressWarnings("unchecked") + private void assertFilterConfiguredWithJwtAuthenticationManager( + AssertableReactiveWebApplicationContext context) { + MatcherSecurityWebFilterChain filterChain = (MatcherSecurityWebFilterChain) context + .getBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN); + List filters = (List) ReflectionTestUtils + .getField(filterChain, "filters"); + AuthenticationWebFilter webFilter = (AuthenticationWebFilter) filters.stream() + .filter((f) -> f instanceof AuthenticationWebFilter).findFirst() + .orElse(null); + ReactiveAuthenticationManager authenticationManager = (ReactiveAuthenticationManager) ReflectionTestUtils + .getField(webFilter, "authenticationManager"); + assertThat(authenticationManager) + .isInstanceOf(JwtReactiveAuthenticationManager.class); + + } + + @EnableWebFluxSecurity + static class TestConfig { + + @Bean + public MapReactiveUserDetailsService userDetailsService() { + return mock(MapReactiveUserDetailsService.class); + } + + } + + @Configuration + static class JwtDecoderConfig { + + @Bean + public ReactiveJwtDecoder decoder() { + return mock(ReactiveJwtDecoder.class); + } + + } + + @Configuration + static class SecurityWebFilterChainConfig { + + @Bean + SecurityWebFilterChain testSpringSecurityFilterChain(ServerHttpSecurity http, + ReactiveJwtDecoder decoder) { + http.authorizeExchange().pathMatchers("/message/**").hasRole("ADMIN") + .anyExchange().authenticated().and().oauth2().resourceServer().jwt() + .jwtDecoder(decoder); + return http.build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-dependencies/pom.xml b/spring-boot-project/spring-boot-dependencies/pom.xml index 667bb98133f..ad9c82f988f 100644 --- a/spring-boot-project/spring-boot-dependencies/pom.xml +++ b/spring-boot-project/spring-boot-dependencies/pom.xml @@ -165,7 +165,7 @@ 1.2.0.RELEASE 2.0.2.RELEASE 1.2.2.RELEASE - 5.1.0.M2 + 5.1.0.BUILD-SNAPSHOT Bean-M1 3.0.3.RELEASE 3.23.1 diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index 130cedd55eb..e5116f40e47 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -3286,7 +3286,10 @@ following example: spring.security.oauth2.resource.jwt.jwk.set-uri=https://example.com/oauth2/default/v1/keys ---- -Alternatively, you can define your own `JwtDecoder` bean. +The same properties are applicable for both servlet and reactive applications. + +Alternatively, you can define your own `JwtDecoder` bean for servlet applications +or a `ReactiveJwtDecoder` for reactive applications. diff --git a/spring-boot-samples/README.adoc b/spring-boot-samples/README.adoc index 8a7378508c6..95df246b2f2 100644 --- a/spring-boot-samples/README.adoc +++ b/spring-boot-samples/README.adoc @@ -137,6 +137,9 @@ The following sample applications are provided: | link:spring-boot-sample-oauth2-resource-server[spring-boot-sample-oauth2-resource-server] | Configure an OAuth2 resource server +| link:spring-boot-sample-reactive-oauth2-resource-server[spring-boot-sample-reactive-oauth2-resource-server] +| Configure a Reactive OAuth2 resource server + | link:spring-boot-sample-parent-context[spring-boot-sample-parent-context] | Application that uses an `ApplicationContext` with a parent diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/pom.xml b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/pom.xml new file mode 100644 index 00000000000..26cecedf791 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-samples + ${revision} + + spring-boot-sample-reactive-oauth2-resource-server + Spring Boot Sample Reactive OAuth2 Resource Server + Spring Boot Sample Reactive Resource Server + + ${basedir}/../.. + + + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.security + spring-security-config + + + org.springframework.security + spring-security-oauth2-jose + + + org.springframework.security + spring-security-oauth2-resource-server + + + + com.squareup.okhttp3 + mockwebserver + 3.9.0 + test + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/java/sample/oauth2/resource/ExampleController.java b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/java/sample/oauth2/resource/ExampleController.java new file mode 100644 index 00000000000..a5cabdd72ef --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/java/sample/oauth2/resource/ExampleController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2018 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 + * + * http://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 sample.oauth2.resource; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class ExampleController { + + @GetMapping("/") + public String index(@AuthenticationPrincipal Jwt jwt) { + return String.format("Hello, %s!", jwt.getSubject()); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/java/sample/oauth2/resource/SampleReactiveOAuth2ResourceServerApplication.java b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/java/sample/oauth2/resource/SampleReactiveOAuth2ResourceServerApplication.java new file mode 100644 index 00000000000..46e3db82792 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/java/sample/oauth2/resource/SampleReactiveOAuth2ResourceServerApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2018 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 + * + * http://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 sample.oauth2.resource; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SampleReactiveOAuth2ResourceServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SampleReactiveOAuth2ResourceServerApplication.class); + } + +} diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/resources/application.yml b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/resources/application.yml new file mode 100644 index 00000000000..2599bf40916 --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/main/resources/application.yml @@ -0,0 +1,8 @@ +spring: + security: + oauth2: + resource: + jwt: + jwk: + # To run the application, replace this with a valid JWK Set URI + set-uri: https://example.com/oauth2/default/v1/keys \ No newline at end of file diff --git a/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/test/java/sample/oauth2/resource/SampleReactiveOAuth2ResourceServerApplicationTests.java b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/test/java/sample/oauth2/resource/SampleReactiveOAuth2ResourceServerApplicationTests.java new file mode 100644 index 00000000000..7660b0c793e --- /dev/null +++ b/spring-boot-samples/spring-boot-sample-reactive-oauth2-resource-server/src/test/java/sample/oauth2/resource/SampleReactiveOAuth2ResourceServerApplicationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2018 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 + * + * http://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 sample.oauth2.resource; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class SampleReactiveOAuth2ResourceServerApplicationTests { + + @Autowired + private WebTestClient webTestClient; + + private static MockWebServer server = new MockWebServer(); + + private static final String VALID_TOKEN = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJzdWJqZWN0Iiwic2NvcGUiOiJtZXNzYWdlOnJlYWQi" + + "LCJleHAiOjQ2ODM4MDUxNDF9.h-j6FKRFdnTdmAueTZCdep45e6DPwqM68ZQ8doIJ1exi9YxAlbWzOwId6Bd0L5YmCmp63gGQgsBUBLzwnZQ8kLUgU" + + "OBEC3UzSWGRqMskCY9_k9pX0iomX6IfF3N0PaYs0WPC4hO1s8wfZQ-6hKQ4KigFi13G9LMLdH58PRMK0pKEvs3gCbHJuEPw-K5ORlpdnleUTQIwIN" + + "afU57cmK3KocTeknPAM_L716sCuSYGvDl6xUTXO7oPdrXhS_EhxLP6KxrpI1uD4Ea_5OWTh7S0Wx5LLDfU6wBG1DowN20d374zepOIEkR-Jnmr_Ql" + + "R44vmRqS5ncrF-1R0EGcPX49U6A"; + + @BeforeClass + public static void setup() throws Exception { + server.start(); + String url = server.url("/.well-known/jwks.json").toString(); + server.enqueue(mockResponse()); + System.setProperty("spring.security.oauth2.resource.jwt.jwk.set-uri", url); + } + + @AfterClass + public static void shutdown() throws Exception { + server.shutdown(); + System.clearProperty("spring.security.oauth2.resource.jwt.jwk.set-uri"); + } + + @Test + public void getWhenValidTokenShouldBeOk() { + this.webTestClient.get().uri("/") + .headers(headers -> headers.setBearerAuth(VALID_TOKEN)).exchange() + .expectStatus().isOk().expectBody(String.class) + .isEqualTo("Hello, subject!"); + } + + @Test + public void getWhenNoTokenShouldBeUnauthorized() { + this.webTestClient.get().uri("/").exchange().expectStatus().isUnauthorized() + .expectHeader().valueEquals(HttpHeaders.WWW_AUTHENTICATE, "Bearer"); + } + + private static MockResponse mockResponse() { + String body = "{\"keys\":[{\"p\":\"2p-ViY7DE9ZrdWQb544m0Jp7Cv03YCSljqfim9pD4ALhObX0OrAznOiowTjwBky9JGffMw" + + "DBVSfJSD9TSU7aH2sbbfi0bZLMdekKAuimudXwUqPDxrrg0BCyvCYgLmKjbVT3zcdylWSog93CNTxGDPzauu-oc0XPNKCXnaDpNvE\"" + + ",\"kty\":\"RSA\",\"q\":\"sP_QYavrpBvSJ86uoKVGj2AGl78CSsAtpf1ybSY5TwUlorXSdqapRbY69Y271b0aMLzlleUn9ZTBO" + + "1dlKV2_dw_lPADHVia8z3pxL-8sUhIXLsgj4acchMk4c9YX-sFh07xENnyZ-_TXm3llPLuL67HUfBC2eKe800TmCYVWc9U\",\"d\"" + + ":\"bn1nFxCQT4KLTHqo8mo9HvHD0cRNRNdWcKNnnEQkCF6tKbt-ILRyQGP8O40axLd7CoNVG9c9p_-g4-2kwCtLJNv_STLtwfpCY7" + + "VN5o6-ZIpfTjiW6duoPrLWq64Hm_4LOBQTiZfUPcLhsuJRHbWqakj-kV_YbUyC2Ocf_dd8IAQcSrAU2SCcDebhDCWwRUFvaa9V5eq0" + + "851S9goaA-AJz-JXyePH6ZFr8JxmWkWxYZ5kdcMD-sm9ZbxE0CaEk32l4fE4hR-L8x2dDtjWA-ahKCZ091z-gV3HWtR2JOjvxoNRjxUo" + + "3UxaGiFJHWNIl0EYUJZu1Cb-5wIlEI7wPx5mwQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"qS0OK4" + + "8M2CIAA6_4Wdw4EbCaAfcTLf5Oy9t5BOF_PFUKqoSpZ6JsT5H0a_4zkjt-oI969v78OTlvBKbmEyKO-KeytzHBAA5CsLmVcz0THrMSg6o" + + "XZqu66MPnvWoZN9FEN5TklPOvBFm8Bg1QZ3k-YMVaM--DLvhaYR95_mqaz50\",\"dp\":\"Too2NozLGD1XrXyhabZvy1E0EuaVFj0UHQ" + + "PDLSpkZ_2g3BK6Art6T0xmE8RYtmqrKIEIdlI3IliAvyvAx_1D7zWTTRaj-xlZyqJFrnXWL7zj8UxT8PkB-r2E-ILZ3NAi1gxIWezlBTZ8" + + "M6NfObDFmbTc_3tJkN_raISo8z_ziIE\",\"dq\":\"U0yhSkY5yOsa9YcMoigGVBWSJLpNHtbg5NypjHrPv8OhWbkOSq7WvSstBkF" + + "k5AtyFvvfZLMLIkWWxxGzV0t6f1MoxBtttLrYYyCxwihiiGFhLbAdSuZ1wnxcqA9bC7UVECvrQmVTpsMs8UupfHKbQBpZ8OWAqrn" + + "uYNNtG4_4Bt0\",\"n\":\"lygtuZj0lJjqOqIWocF8Bb583QDdq-aaFg8PesOp2-EDda6GqCpL-_NZVOflNGX7XIgjsWHcPsQHs" + + "V9gWuOzSJ0iEuWvtQ6eGBP5M6m7pccLNZfwUse8Cb4Ngx3XiTlyuqM7pv0LPyppZusfEHVEdeelou7Dy9k0OQ_nJTI3b2E1WBoHC5" + + "8CJ453lo4gcBm1efURN3LIVc1V9NQY_ESBKVdwqYyoJPEanURLVGRd6cQKn6YrCbbIRHjqAyqOE-z3KmgDJnPriljfR5XhSGyM9eq" + + "D9Xpy6zu_MAeMJJfSArp857zLPk-Wf5VP9STAcjyfdBIybMKnwBYr2qHMT675hQ\"}]}"; + return new MockResponse() + .setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .setResponseCode(200).setBody(body); + } + +}