Add sample for PKI Mutual-TLS client authentication method

Issue gh-1558
This commit is contained in:
Joe Grandja
2024-03-15 10:13:25 -04:00
parent 8361dabdbf
commit d6a87532a9
11 changed files with 253 additions and 17 deletions

View File

@@ -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);
};
}
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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>