Browse Source

Add sample for self-signed certificate Mutual-TLS client authentication method

Issue gh-1559
Joe Grandja 1 year ago
parent
commit
9bd0043cc6

+ 2 - 0
samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java

@@ -169,12 +169,14 @@ public class AuthorizationServerConfig {
 		RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString())
 				.clientId("mtls-demo-client")
 				.clientAuthenticationMethod(new ClientAuthenticationMethod("tls_client_auth"))
+				.clientAuthenticationMethod(new ClientAuthenticationMethod("self_signed_tls_client_auth"))
 				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
 				.scope("message.read")
 				.scope("message.write")
 				.clientSettings(
 						ClientSettings.builder()
 								.x509CertificateSubjectDN("CN=demo-client-sample,OU=Spring Samples,O=Spring,C=US")
+								.jwkSetUrl("http://127.0.0.1:8080/jwks")
 								.build()
 				)
 				.build();

+ 21 - 2
samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java

@@ -43,8 +43,8 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 @Configuration(proxyBeanMethods = false)
 public class RestTemplateConfig {
 
-	@Bean
-	Supplier<ClientHttpRequestFactory> clientHttpRequestFactory(SslBundles sslBundles) {
+	@Bean("default-client-http-request-factory")
+	Supplier<ClientHttpRequestFactory> defaultClientHttpRequestFactory(SslBundles sslBundles) {
 		return () -> {
 			SslBundle sslBundle = sslBundles.getBundle("demo-client");
 			final SSLContext sslContext = sslBundle.createSslContext();
@@ -63,4 +63,23 @@ public class RestTemplateConfig {
 		};
 	}
 
+	@Bean("self-signed-demo-client-http-request-factory")
+	Supplier<ClientHttpRequestFactory> selfSignedDemoClientHttpRequestFactory(SslBundles sslBundles) {
+		return () -> {
+			SslBundle sslBundle = sslBundles.getBundle("self-signed-demo-client");
+			final SSLContext sslContext = sslBundle.createSslContext();
+			final SSLConnectionSocketFactory sslConnectionSocketFactory =
+					new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
+			final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
+					.register("https", sslConnectionSocketFactory)
+					.build();
+			final BasicHttpClientConnectionManager connectionManager =
+					new BasicHttpClientConnectionManager(socketFactoryRegistry);
+			final CloseableHttpClient httpClient = HttpClients.custom()
+					.setConnectionManager(connectionManager)
+					.build();
+			return new HttpComponentsClientHttpRequestFactory(httpClient);
+		};
+	}
+
 }

+ 2 - 2
samples/demo-client/src/main/java/sample/config/SecurityConfig.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 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.
@@ -49,7 +49,7 @@ public class SecurityConfig {
 		http
 			.authorizeHttpRequests(authorize ->
 				authorize
-					.requestMatchers("/logged-out").permitAll()
+					.requestMatchers("/jwks", "/logged-out").permitAll()
 					.anyRequest().authenticated()
 			)
 			.oauth2Login(oauth2Login ->

+ 43 - 3
samples/demo-client/src/main/java/sample/config/WebClientConfig.java

@@ -20,6 +20,7 @@ import java.util.function.Supplier;
 
 import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider;
 
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.web.client.RestTemplateBuilder;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -52,8 +53,47 @@ import org.springframework.web.reactive.function.client.WebClient;
 @Configuration(proxyBeanMethods = false)
 public class WebClientConfig {
 
-	@Bean
-	public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
+	@Bean("default-client-web-client")
+	public WebClient defaultClientWebClient(OAuth2AuthorizedClientManager authorizedClientManager) {
+		ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
+				new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
+		// @formatter:off
+		return WebClient.builder()
+				.apply(oauth2Client.oauth2Configuration())
+				.build();
+		// @formatter:on
+	}
+
+	@Bean("self-signed-demo-client-web-client")
+	public WebClient selfSignedDemoClientWebClient(
+			ClientRegistrationRepository clientRegistrationRepository,
+			OAuth2AuthorizedClientRepository authorizedClientRepository,
+			RestTemplateBuilder restTemplateBuilder,
+			@Qualifier("self-signed-demo-client-http-request-factory") Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
+
+		// @formatter:off
+		RestTemplate restTemplate = restTemplateBuilder
+				.requestFactory(clientHttpRequestFactory)
+				.messageConverters(Arrays.asList(
+						new FormHttpMessageConverter(),
+						new OAuth2AccessTokenResponseHttpMessageConverter()))
+				.errorHandler(new OAuth2ErrorResponseErrorHandler())
+				.build();
+		// @formatter:on
+
+		// @formatter:off
+		OAuth2AuthorizedClientProvider authorizedClientProvider =
+				OAuth2AuthorizedClientProviderBuilder.builder()
+						.clientCredentials(clientCredentials ->
+								clientCredentials.accessTokenResponseClient(
+										createClientCredentialsTokenResponseClient(restTemplate)))
+						.build();
+		// @formatter:on
+
+		DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
+				clientRegistrationRepository, authorizedClientRepository);
+		authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
+
 		ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
 				new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
 		// @formatter:off
@@ -68,7 +108,7 @@ public class WebClientConfig {
 			ClientRegistrationRepository clientRegistrationRepository,
 			OAuth2AuthorizedClientRepository authorizedClientRepository,
 			RestTemplateBuilder restTemplateBuilder,
-			Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
+			@Qualifier("default-client-http-request-factory") Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
 
 		// @formatter:off
 		RestTemplate restTemplate = restTemplateBuilder

+ 27 - 7
samples/demo-client/src/main/java/sample/web/AuthorizationController.java

@@ -17,6 +17,7 @@ package sample.web;
 
 import jakarta.servlet.http.HttpServletRequest;
 
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
 import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
@@ -39,14 +40,18 @@ import static org.springframework.security.oauth2.client.web.reactive.function.c
  */
 @Controller
 public class AuthorizationController {
-	private final WebClient webClient;
+	private final WebClient defaultClientWebClient;
+	private final WebClient selfSignedDemoClientWebClient;
 	private final String messagesBaseUri;
 	private final String userMessagesBaseUri;
 
-	public AuthorizationController(WebClient webClient,
+	public AuthorizationController(
+			@Qualifier("default-client-web-client") WebClient defaultClientWebClient,
+			@Qualifier("self-signed-demo-client-web-client") WebClient selfSignedDemoClientWebClient,
 			@Value("${messages.base-uri}") String messagesBaseUri,
 			@Value("${user-messages.base-uri}") String userMessagesBaseUri) {
-		this.webClient = webClient;
+		this.defaultClientWebClient = defaultClientWebClient;
+		this.selfSignedDemoClientWebClient = selfSignedDemoClientWebClient;
 		this.messagesBaseUri = messagesBaseUri;
 		this.userMessagesBaseUri = userMessagesBaseUri;
 	}
@@ -56,7 +61,7 @@ public class AuthorizationController {
 			@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code")
 					OAuth2AuthorizedClient authorizedClient) {
 
-		String[] messages = this.webClient
+		String[] messages = this.defaultClientWebClient
 				.get()
 				.uri(this.messagesBaseUri)
 				.attributes(oauth2AuthorizedClient(authorizedClient))
@@ -87,7 +92,7 @@ public class AuthorizationController {
 	@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=client_secret"})
 	public String clientCredentialsGrantUsingClientSecret(Model model) {
 
-		String[] messages = this.webClient
+		String[] messages = this.defaultClientWebClient
 				.get()
 				.uri(this.messagesBaseUri)
 				.attributes(clientRegistrationId("messaging-client-client-credentials"))
@@ -102,7 +107,7 @@ public class AuthorizationController {
 	@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=mtls"})
 	public String clientCredentialsGrantUsingMutualTLS(Model model) {
 
-		String[] messages = this.webClient
+		String[] messages = this.defaultClientWebClient
 				.get()
 				.uri(this.messagesBaseUri)
 				.attributes(clientRegistrationId("mtls-demo-client-client-credentials"))
@@ -114,10 +119,25 @@ public class AuthorizationController {
 		return "index";
 	}
 
+	@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=self_signed_mtls"})
+	public String clientCredentialsGrantUsingSelfSignedMutualTLS(Model model) {
+
+		String[] messages = this.selfSignedDemoClientWebClient
+				.get()
+				.uri(this.messagesBaseUri)
+				.attributes(clientRegistrationId("mtls-self-signed-demo-client-client-credentials"))
+				.retrieve()
+				.bodyToMono(String[].class)
+				.block();
+		model.addAttribute("messages", messages);
+
+		return "index";
+	}
+
 	@GetMapping(value = "/authorize", params = "grant_type=token_exchange")
 	public String tokenExchangeGrant(Model model) {
 
-		String[] messages = this.webClient
+		String[] messages = this.defaultClientWebClient
 				.get()
 				.uri(this.userMessagesBaseUri)
 				.attributes(clientRegistrationId("user-client-authorization-code"))

+ 5 - 2
samples/demo-client/src/main/java/sample/web/DeviceController.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2024 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.
@@ -22,6 +22,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.core.ParameterizedTypeReference;
 import org.springframework.http.HttpStatus;
@@ -72,7 +73,9 @@ public class DeviceController {
 
 	private final String messagesBaseUri;
 
-	public DeviceController(ClientRegistrationRepository clientRegistrationRepository, WebClient webClient,
+	public DeviceController(
+			ClientRegistrationRepository clientRegistrationRepository,
+			@Qualifier("default-client-web-client") WebClient webClient,
 			@Value("${messages.base-uri}") String messagesBaseUri) {
 
 		this.clientRegistrationRepository = clientRegistrationRepository;

+ 68 - 0
samples/demo-client/src/main/java/sample/web/JwkSetController.java

@@ -0,0 +1,68 @@
+/*
+ * Copyright 2020-2024 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.web;
+
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.interfaces.RSAPublicKey;
+import java.util.Collections;
+import java.util.Map;
+import java.util.UUID;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.KeyUse;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.util.Base64;
+
+import org.springframework.boot.ssl.SslBundle;
+import org.springframework.boot.ssl.SslBundles;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Joe Grandja
+ * @since 1.3
+ */
+@RestController
+public class JwkSetController {
+	private final JWKSet jwkSet;
+
+	public JwkSetController(SslBundles sslBundles) throws Exception {
+		this.jwkSet = initJwkSet(sslBundles);
+	}
+
+	@GetMapping("/jwks")
+	public Map<String, Object> getJwkSet() {
+		return this.jwkSet.toJSONObject();
+	}
+
+	private static JWKSet initJwkSet(SslBundles sslBundles) throws Exception {
+		SslBundle sslBundle = sslBundles.getBundle("self-signed-demo-client");
+		KeyStore keyStore = sslBundle.getStores().getKeyStore();
+		String alias = sslBundle.getKey().getAlias();
+
+		Certificate certificate = keyStore.getCertificate(alias);
+
+		RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) certificate.getPublicKey())
+				.keyUse(KeyUse.SIGNATURE)
+				.keyID(UUID.randomUUID().toString())
+				.x509CertChain(Collections.singletonList(Base64.encode(certificate.getEncoded())))
+				.build();
+
+		return new JWKSet(rsaKey);
+	}
+
+}

+ 19 - 0
samples/demo-client/src/main/resources/application.yml

@@ -24,6 +24,18 @@ spring:
             location: classpath:keystore.p12
             password: password
             type: PKCS12
+        self-signed-demo-client:
+          key:
+            alias: self-signed-demo-client-sample
+            password: password
+          keystore:
+            location: classpath:keystore-self-signed.p12
+            password: password
+            type: PKCS12
+          truststore:
+            location: classpath:keystore-self-signed.p12
+            password: password
+            type: PKCS12
   thymeleaf:
     cache: false
   security:
@@ -75,6 +87,13 @@ spring:
             authorization-grant-type: client_credentials
             scope: message.read,message.write
             client-name: mtls-demo-client-client-credentials
+          mtls-self-signed-demo-client-client-credentials:
+            provider: spring-tls
+            client-id: mtls-demo-client
+            client-authentication-method: self_signed_tls_client_auth
+            authorization-grant-type: client_credentials
+            scope: message.read,message.write
+            client-name: mtls-self-signed-demo-client-client-credentials
         provider:
           spring:
             issuer-uri: http://localhost:9000

+ 1 - 0
samples/demo-client/src/main/resources/templates/page-templates.html

@@ -26,6 +26,7 @@
                         <li><a class="dropdown-item" href="/authorize?grant_type=authorization_code" th:href="@{/authorize?grant_type=authorization_code}">Authorization Code</a></li>
                         <li><a class="dropdown-item" href="/authorize?grant_type=client_credentials&client_auth=client_secret" th:href="@{/authorize?grant_type=client_credentials&client_auth=client_secret}">Client Credentials (client_secret_basic)</a></li>
                         <li><a class="dropdown-item" href="/authorize?grant_type=client_credentials&client_auth=mtls" th:href="@{/authorize?grant_type=client_credentials&client_auth=mtls}">Client Credentials (tls_client_auth)</a></li>
+                        <li><a class="dropdown-item" href="/authorize?grant_type=client_credentials&client_auth=self_signed_mtls" th:href="@{/authorize?grant_type=client_credentials&client_auth=self_signed_mtls}">Client Credentials (self_signed_tls_client_auth)</a></li>
                         <li><a class="dropdown-item" href="/authorize?grant_type=token_exchange" th:href="@{/authorize?grant_type=token_exchange}">Token Exchange</a></li>
                         <li><a class="dropdown-item" href="/authorize?grant_type=device_code" th:href="@{/authorize?grant_type=device_code}">Device Code</a></li>
                     </ul>