浏览代码

Add sample for PKI Mutual-TLS client authentication method

Issue gh-1558
Joe Grandja 1 年之前
父节点
当前提交
d6a87532a9

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

@@ -131,7 +131,7 @@ public class AuthorizationServerConfig {
 	// @formatter:off
 	@Bean
 	public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
-		RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
+		RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString())
 				.clientId("messaging-client")
 				.clientSecret("{noop}secret")
 				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
@@ -166,11 +166,25 @@ public class AuthorizationServerConfig {
 				.scope("message.read")
 				.build();
 
+		RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString())
+				.clientId("mtls-demo-client")
+				.clientAuthenticationMethod(new ClientAuthenticationMethod("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")
+								.build()
+				)
+				.build();
+
 		// Save registered client's in db as if in-memory
 		JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
-		registeredClientRepository.save(registeredClient);
+		registeredClientRepository.save(messagingClient);
 		registeredClientRepository.save(deviceClient);
 		registeredClientRepository.save(tokenExchangeClient);
+		registeredClientRepository.save(mtlsDemoClient);
 
 		return registeredClientRepository;
 	}

+ 46 - 0
samples/demo-authorizationserver/src/main/java/sample/config/TomcatServerConfig.java

@@ -0,0 +1,46 @@
+/*
+ * 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.config;
+
+import org.apache.catalina.connector.Connector;
+
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author Joe Grandja
+ * @since 1.3
+ */
+@Configuration(proxyBeanMethods = false)
+public class TomcatServerConfig {
+
+	@Bean
+	public WebServerFactoryCustomizer<TomcatServletWebServerFactory> connectorCustomizer() {
+		return (tomcat) -> tomcat.addAdditionalTomcatConnectors(createHttpConnector());
+	}
+
+	private Connector createHttpConnector() {
+		Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
+		connector.setScheme("http");
+		connector.setPort(9000);
+		connector.setSecure(false);
+		connector.setRedirectPort(9443);
+		return connector;
+	}
+
+}

+ 19 - 1
samples/demo-authorizationserver/src/main/resources/application.yml

@@ -1,7 +1,25 @@
 server:
-  port: 9000
+  port: 9443
+  ssl:
+    bundle: demo-authorizationserver
+    client-auth: want
 
 spring:
+  ssl:
+    bundle:
+      jks:
+        demo-authorizationserver:
+          key:
+            alias: demo-authorizationserver-sample
+            password: password
+          keystore:
+            location: classpath:keystore.p12
+            password: password
+            type: PKCS12
+          truststore:
+            location: classpath:keystore.p12
+            password: password
+            type: PKCS12
   security:
     oauth2:
       client:

+ 1 - 0
samples/demo-client/samples-demo-client.gradle

@@ -23,6 +23,7 @@ dependencies {
 	implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
 	implementation "org.springframework:spring-webflux"
 	implementation "io.projectreactor.netty:reactor-netty"
+	implementation "org.apache.httpcomponents.client5:httpclient5"
 	implementation "org.webjars:webjars-locator-core"
 	implementation "org.webjars:bootstrap:5.2.3"
 	implementation "org.webjars:popper.js:2.9.3"

+ 66 - 0
samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java

@@ -0,0 +1,66 @@
+/*
+ * 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.config;
+
+import java.util.function.Supplier;
+
+import javax.net.ssl.SSLContext;
+
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.client5.http.impl.classic.HttpClients;
+import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
+import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
+import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
+import org.apache.hc.core5.http.config.Registry;
+import org.apache.hc.core5.http.config.RegistryBuilder;
+
+import org.springframework.boot.ssl.SslBundle;
+import org.springframework.boot.ssl.SslBundles;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+
+/**
+ * @author Joe Grandja
+ * @since 1.3
+ */
+@Configuration(proxyBeanMethods = false)
+public class RestTemplateConfig {
+
+	@Bean
+	Supplier<ClientHttpRequestFactory> clientHttpRequestFactory(SslBundles sslBundles) {
+		return () -> {
+			SslBundle sslBundle = sslBundles.getBundle("demo-client");
+			final SSLContext sslContext = sslBundle.createSslContext();
+			final SSLConnectionSocketFactory sslConnectionSocketFactory =
+					new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
+			final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
+					.register("http", PlainConnectionSocketFactory.getSocketFactory())
+					.register("https", sslConnectionSocketFactory)
+					.build();
+			final BasicHttpClientConnectionManager connectionManager =
+					new BasicHttpClientConnectionManager(socketFactoryRegistry);
+			final CloseableHttpClient httpClient = HttpClients.custom()
+					.setConnectionManager(connectionManager)
+					.build();
+			return new HttpComponentsClientHttpRequestFactory(httpClient);
+		};
+	}
+
+}

+ 53 - 4
samples/demo-client/src/main/java/sample/config/WebClientConfig.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.
@@ -15,17 +15,33 @@
  */
 package sample.config;
 
+import java.util.Arrays;
+import java.util.function.Supplier;
+
 import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider;
 
+import org.springframework.boot.web.client.RestTemplateBuilder;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.converter.FormHttpMessageConverter;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
 import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
+import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
+import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
+import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter;
+import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
 import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
 import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
 import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
 import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
 import org.springframework.web.reactive.function.client.WebClient;
 
 /**
@@ -33,7 +49,7 @@ import org.springframework.web.reactive.function.client.WebClient;
  * @author Steve Riesenberg
  * @since 0.0.1
  */
-@Configuration
+@Configuration(proxyBeanMethods = false)
 public class WebClientConfig {
 
 	@Bean
@@ -50,14 +66,28 @@ public class WebClientConfig {
 	@Bean
 	public OAuth2AuthorizedClientManager authorizedClientManager(
 			ClientRegistrationRepository clientRegistrationRepository,
-			OAuth2AuthorizedClientRepository authorizedClientRepository) {
+			OAuth2AuthorizedClientRepository authorizedClientRepository,
+			RestTemplateBuilder restTemplateBuilder,
+			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()
 						.authorizationCode()
 						.refreshToken()
-						.clientCredentials()
+						.clientCredentials(clientCredentials ->
+								clientCredentials.accessTokenResponseClient(
+										createClientCredentialsTokenResponseClient(restTemplate)))
 						.provider(new DeviceCodeOAuth2AuthorizedClientProvider())
 						.build();
 		// @formatter:on
@@ -73,4 +103,23 @@ public class WebClientConfig {
 		return authorizedClientManager;
 	}
 
+	private static OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> createClientCredentialsTokenResponseClient(
+			RestTemplate restTemplate) {
+		DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
+				new DefaultClientCredentialsTokenResponseClient();
+		clientCredentialsTokenResponseClient.setRestOperations(restTemplate);
+
+		OAuth2ClientCredentialsGrantRequestEntityConverter clientCredentialsGrantRequestEntityConverter =
+				new OAuth2ClientCredentialsGrantRequestEntityConverter();
+		clientCredentialsGrantRequestEntityConverter.addParametersConverter(authorizationGrantRequest -> {
+			MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
+			// client_id parameter is required for tls_client_auth method
+			parameters.add(OAuth2ParameterNames.CLIENT_ID, authorizationGrantRequest.getClientRegistration().getClientId());
+			return parameters;
+		});
+		clientCredentialsTokenResponseClient.setRequestEntityConverter(clientCredentialsGrantRequestEntityConverter);
+
+		return clientCredentialsTokenResponseClient;
+	}
+
 }

+ 17 - 2
samples/demo-client/src/main/java/sample/web/AuthorizationController.java

@@ -84,8 +84,8 @@ public class AuthorizationController {
 		return "index";
 	}
 
-	@GetMapping(value = "/authorize", params = "grant_type=client_credentials")
-	public String clientCredentialsGrant(Model model) {
+	@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=client_secret"})
+	public String clientCredentialsGrantUsingClientSecret(Model model) {
 
 		String[] messages = this.webClient
 				.get()
@@ -99,6 +99,21 @@ public class AuthorizationController {
 		return "index";
 	}
 
+	@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=mtls"})
+	public String clientCredentialsGrantUsingMutualTLS(Model model) {
+
+		String[] messages = this.webClient
+				.get()
+				.uri(this.messagesBaseUri)
+				.attributes(clientRegistrationId("mtls-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) {
 

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

@@ -9,6 +9,21 @@ logging:
     org.springframework.security.oauth2: INFO
 
 spring:
+  ssl:
+    bundle:
+      jks:
+        demo-client:
+          key:
+            alias: demo-client-sample
+            password: password
+          keystore:
+            location: classpath:keystore.p12
+            password: password
+            type: PKCS12
+          truststore:
+            location: classpath:keystore.p12
+            password: password
+            type: PKCS12
   thymeleaf:
     cache: false
   security:
@@ -53,9 +68,18 @@ spring:
             authorization-grant-type: urn:ietf:params:oauth:grant-type:device_code
             scope: message.read,message.write
             client-name: messaging-client-device-code
+          mtls-demo-client-client-credentials:
+            provider: spring-tls
+            client-id: mtls-demo-client
+            client-authentication-method: tls_client_auth
+            authorization-grant-type: client_credentials
+            scope: message.read,message.write
+            client-name: mtls-demo-client-client-credentials
         provider:
           spring:
             issuer-uri: http://localhost:9000
+          spring-tls:
+            token-uri: https://localhost:9443/oauth2/token
 
 messages:
   base-uri: http://127.0.0.1:8090/messages

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

@@ -24,7 +24,8 @@
                     <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Authorize</a>
                     <ul class="dropdown-menu">
                         <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" th:href="@{/authorize?grant_type=client_credentials}">Client Credentials</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=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>

+ 8 - 6
samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2020-2022 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.
@@ -17,6 +17,7 @@ package sample.config;
 
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
 import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.web.SecurityFilterChain;
@@ -34,11 +35,12 @@ public class ResourceServerConfig {
 	SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
 		http
 			.securityMatcher("/messages/**")
-				.authorizeHttpRequests()
-					.requestMatchers("/messages/**").hasAuthority("SCOPE_message.read")
-					.and()
-			.oauth2ResourceServer()
-				.jwt();
+				.authorizeHttpRequests(authorize ->
+						authorize.requestMatchers("/messages/**").hasAuthority("SCOPE_message.read")
+				)
+				.oauth2ResourceServer(oauth2ResourceServer ->
+						oauth2ResourceServer.jwt(Customizer.withDefaults())
+				);
 		return http.build();
 	}
 	// @formatter:on

+ 1 - 1
samples/messages-resource/src/main/resources/application.yml

@@ -14,4 +14,4 @@ spring:
     oauth2:
       resourceserver:
         jwt:
-          issuer-uri: http://localhost:9000
+          jwk-set-uri: http://localhost:9000/oauth2/jwks