From a21fa1494aadce59219c6e0150b0b2e5fd484a24 Mon Sep 17 00:00:00 2001 From: Alexey Nesterov Date: Thu, 27 Dec 2018 17:53:20 +0300 Subject: [PATCH] Add an example and basic integration test for x509 authentication [gh #5038] --- ...-security-samples-boot-webflux-x509.gradle | 11 +++ .../src/main/java/sample/MeController.java | 40 ++++++++ .../java/sample/WebfluxX509Application.java | 58 +++++++++++ .../src/main/resources/application.yml | 8 ++ .../src/main/resources/certs/curl_app.sh | 2 + .../src/main/resources/certs/server.p12 | Bin 0 -> 9319 bytes .../sample/WebfluxX509ApplicationTest.java | 90 ++++++++++++++++++ 7 files changed, 209 insertions(+) create mode 100644 samples/boot/webflux-x509/spring-security-samples-boot-webflux-x509.gradle create mode 100644 samples/boot/webflux-x509/src/main/java/sample/MeController.java create mode 100644 samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java create mode 100644 samples/boot/webflux-x509/src/main/resources/application.yml create mode 100644 samples/boot/webflux-x509/src/main/resources/certs/curl_app.sh create mode 100644 samples/boot/webflux-x509/src/main/resources/certs/server.p12 create mode 100644 samples/boot/webflux-x509/src/test/java/sample/WebfluxX509ApplicationTest.java diff --git a/samples/boot/webflux-x509/spring-security-samples-boot-webflux-x509.gradle b/samples/boot/webflux-x509/spring-security-samples-boot-webflux-x509.gradle new file mode 100644 index 0000000000..57196d2b2b --- /dev/null +++ b/samples/boot/webflux-x509/spring-security-samples-boot-webflux-x509.gradle @@ -0,0 +1,11 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile project(':spring-security-core') + compile project(':spring-security-config') + compile project(':spring-security-web') + compile 'org.springframework.boot:spring-boot-starter-webflux' + + testCompile project(':spring-security-test') + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/boot/webflux-x509/src/main/java/sample/MeController.java b/samples/boot/webflux-x509/src/main/java/sample/MeController.java new file mode 100644 index 0000000000..f7a3958784 --- /dev/null +++ b/samples/boot/webflux-x509/src/main/java/sample/MeController.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-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 + * + * 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 sample; + +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +/** + * @author Alexey Nesterov + * @since 5.2 + */ +@RestController +@RequestMapping("/me") +public class MeController { + + @GetMapping + public Mono me() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(authentication -> "Hello, " + authentication.getName()); + } +} diff --git a/samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java b/samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java new file mode 100644 index 0000000000..a6871f7c72 --- /dev/null +++ b/samples/boot/webflux-x509/src/main/java/sample/WebfluxX509Application.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-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 + * + * 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 sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.web.server.SecurityWebFilterChain; + +/** + * @author Alexey Nesterov + * @since 5.2 + */ +@SpringBootApplication +public class WebfluxX509Application { + + @Bean + public ReactiveUserDetailsService reactiveUserDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("client").password("").authorities("ROLE_USER").build() + ); + } + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + http + .x509() + .and() + .authorizeExchange() + .anyExchange().authenticated(); + // @formatter:on + + return http.build(); + } + + public static void main(String[] args) { + SpringApplication.run(WebfluxX509Application.class); + } +} diff --git a/samples/boot/webflux-x509/src/main/resources/application.yml b/samples/boot/webflux-x509/src/main/resources/application.yml new file mode 100644 index 0000000000..cdeb85fa7b --- /dev/null +++ b/samples/boot/webflux-x509/src/main/resources/application.yml @@ -0,0 +1,8 @@ +server: + port: 8443 + ssl: + key-store: 'classpath:./certs/server.p12' + key-store-password: 'password' + client-auth: need + trust-store: 'classpath:./certs/server.p12' + trust-store-password: 'password' diff --git a/samples/boot/webflux-x509/src/main/resources/certs/curl_app.sh b/samples/boot/webflux-x509/src/main/resources/certs/curl_app.sh new file mode 100644 index 0000000000..dbf7af4b43 --- /dev/null +++ b/samples/boot/webflux-x509/src/main/resources/certs/curl_app.sh @@ -0,0 +1,2 @@ + curl -vvvv --cacert out/DevCA.crt --cert out/localhost.crt --key out/localhost.key https://localhost:8443/me + diff --git a/samples/boot/webflux-x509/src/main/resources/certs/server.p12 b/samples/boot/webflux-x509/src/main/resources/certs/server.p12 new file mode 100644 index 0000000000000000000000000000000000000000..8096cc73b9f82ff372925b81973f1e738917429c GIT binary patch literal 9319 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 literal 0 HcmV?d00001 diff --git a/samples/boot/webflux-x509/src/test/java/sample/WebfluxX509ApplicationTest.java b/samples/boot/webflux-x509/src/test/java/sample/WebfluxX509ApplicationTest.java new file mode 100644 index 0000000000..604692d5ad --- /dev/null +++ b/samples/boot/webflux-x509/src/test/java/sample/WebfluxX509ApplicationTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-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 + * + * 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 sample; + +import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.SslContextBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; +import reactor.netty.http.client.HttpClient; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class WebfluxX509ApplicationTest { + + @LocalServerPort + int port; + + @Test + public void shouldExtractAuthenticationFromCertificate() throws Exception { + WebTestClient webTestClient = createWebTestClientWithClientCertificate(); + webTestClient + .get().uri("/me") + .exchange() + .expectStatus().isOk() + .expectBody() + .consumeWith(result -> { + String responseBody = new String(result.getResponseBody()); + assertThat(responseBody).contains("Hello, client"); + }); + } + + private WebTestClient createWebTestClientWithClientCertificate() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableEntryException { + ClassPathResource serverKeystore = new ClassPathResource("/certs/server.p12"); + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(serverKeystore.getInputStream(), "password".toCharArray()); + + X509Certificate devCA = (X509Certificate) keyStore.getCertificate("DevCA"); + + X509Certificate clientCrt = (X509Certificate) keyStore.getCertificate("client"); + KeyStore.Entry keyStoreEntry = keyStore.getEntry("client", + new KeyStore.PasswordProtection("password".toCharArray())); + PrivateKey clientKey = ((KeyStore.PrivateKeyEntry) keyStoreEntry).getPrivateKey(); + + SslContextBuilder sslContextBuilder = SslContextBuilder + .forClient().clientAuth(ClientAuth.REQUIRE) + .trustManager(devCA) + .keyManager(clientKey, clientCrt); + + HttpClient httpClient = HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContextBuilder)); + ClientHttpConnector httpConnector = new ReactorClientHttpConnector(httpClient); + + return WebTestClient + .bindToServer(httpConnector) + .baseUrl("https://localhost:" + port) + .build(); + } +}