Skip to content

Commit d6a8753

Browse files
committed
Add sample for PKI Mutual-TLS client authentication method
Issue gh-1558
1 parent 8361dab commit d6a8753

File tree

11 files changed

+253
-17
lines changed

11 files changed

+253
-17
lines changed

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain(
131131
// @formatter:off
132132
@Bean
133133
public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
134-
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
134+
RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString())
135135
.clientId("messaging-client")
136136
.clientSecret("{noop}secret")
137137
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
@@ -166,11 +166,25 @@ public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jd
166166
.scope("message.read")
167167
.build();
168168

169+
RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString())
170+
.clientId("mtls-demo-client")
171+
.clientAuthenticationMethod(new ClientAuthenticationMethod("tls_client_auth"))
172+
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
173+
.scope("message.read")
174+
.scope("message.write")
175+
.clientSettings(
176+
ClientSettings.builder()
177+
.x509CertificateSubjectDN("CN=demo-client-sample,OU=Spring Samples,O=Spring,C=US")
178+
.build()
179+
)
180+
.build();
181+
169182
// Save registered client's in db as if in-memory
170183
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
171-
registeredClientRepository.save(registeredClient);
184+
registeredClientRepository.save(messagingClient);
172185
registeredClientRepository.save(deviceClient);
173186
registeredClientRepository.save(tokenExchangeClient);
187+
registeredClientRepository.save(mtlsDemoClient);
174188

175189
return registeredClientRepository;
176190
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2020-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample.config;
17+
18+
import org.apache.catalina.connector.Connector;
19+
20+
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
21+
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Configuration;
24+
25+
/**
26+
* @author Joe Grandja
27+
* @since 1.3
28+
*/
29+
@Configuration(proxyBeanMethods = false)
30+
public class TomcatServerConfig {
31+
32+
@Bean
33+
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> connectorCustomizer() {
34+
return (tomcat) -> tomcat.addAdditionalTomcatConnectors(createHttpConnector());
35+
}
36+
37+
private Connector createHttpConnector() {
38+
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
39+
connector.setScheme("http");
40+
connector.setPort(9000);
41+
connector.setSecure(false);
42+
connector.setRedirectPort(9443);
43+
return connector;
44+
}
45+
46+
}

samples/demo-authorizationserver/src/main/resources/application.yml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,25 @@
11
server:
2-
port: 9000
2+
port: 9443
3+
ssl:
4+
bundle: demo-authorizationserver
5+
client-auth: want
36

47
spring:
8+
ssl:
9+
bundle:
10+
jks:
11+
demo-authorizationserver:
12+
key:
13+
alias: demo-authorizationserver-sample
14+
password: password
15+
keystore:
16+
location: classpath:keystore.p12
17+
password: password
18+
type: PKCS12
19+
truststore:
20+
location: classpath:keystore.p12
21+
password: password
22+
type: PKCS12
523
security:
624
oauth2:
725
client:

samples/demo-client/samples-demo-client.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dependencies {
2323
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
2424
implementation "org.springframework:spring-webflux"
2525
implementation "io.projectreactor.netty:reactor-netty"
26+
implementation "org.apache.httpcomponents.client5:httpclient5"
2627
implementation "org.webjars:webjars-locator-core"
2728
implementation "org.webjars:bootstrap:5.2.3"
2829
implementation "org.webjars:popper.js:2.9.3"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2020-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample.config;
17+
18+
import java.util.function.Supplier;
19+
20+
import javax.net.ssl.SSLContext;
21+
22+
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
23+
import org.apache.hc.client5.http.impl.classic.HttpClients;
24+
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
25+
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
26+
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
27+
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
28+
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
29+
import org.apache.hc.core5.http.config.Registry;
30+
import org.apache.hc.core5.http.config.RegistryBuilder;
31+
32+
import org.springframework.boot.ssl.SslBundle;
33+
import org.springframework.boot.ssl.SslBundles;
34+
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Configuration;
36+
import org.springframework.http.client.ClientHttpRequestFactory;
37+
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
38+
39+
/**
40+
* @author Joe Grandja
41+
* @since 1.3
42+
*/
43+
@Configuration(proxyBeanMethods = false)
44+
public class RestTemplateConfig {
45+
46+
@Bean
47+
Supplier<ClientHttpRequestFactory> clientHttpRequestFactory(SslBundles sslBundles) {
48+
return () -> {
49+
SslBundle sslBundle = sslBundles.getBundle("demo-client");
50+
final SSLContext sslContext = sslBundle.createSslContext();
51+
final SSLConnectionSocketFactory sslConnectionSocketFactory =
52+
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
53+
final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
54+
.register("http", PlainConnectionSocketFactory.getSocketFactory())
55+
.register("https", sslConnectionSocketFactory)
56+
.build();
57+
final BasicHttpClientConnectionManager connectionManager =
58+
new BasicHttpClientConnectionManager(socketFactoryRegistry);
59+
final CloseableHttpClient httpClient = HttpClients.custom()
60+
.setConnectionManager(connectionManager)
61+
.build();
62+
return new HttpComponentsClientHttpRequestFactory(httpClient);
63+
};
64+
}
65+
66+
}

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

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2023 the original author or authors.
2+
* Copyright 2020-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,25 +15,41 @@
1515
*/
1616
package sample.config;
1717

18+
import java.util.Arrays;
19+
import java.util.function.Supplier;
20+
1821
import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider;
1922

23+
import org.springframework.boot.web.client.RestTemplateBuilder;
2024
import org.springframework.context.annotation.Bean;
2125
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.http.client.ClientHttpRequestFactory;
27+
import org.springframework.http.converter.FormHttpMessageConverter;
2228
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
2329
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
2430
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
31+
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
32+
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
33+
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
34+
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter;
35+
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
2536
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
2637
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
2738
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
2839
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
40+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
41+
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
42+
import org.springframework.util.LinkedMultiValueMap;
43+
import org.springframework.util.MultiValueMap;
44+
import org.springframework.web.client.RestTemplate;
2945
import org.springframework.web.reactive.function.client.WebClient;
3046

3147
/**
3248
* @author Joe Grandja
3349
* @author Steve Riesenberg
3450
* @since 0.0.1
3551
*/
36-
@Configuration
52+
@Configuration(proxyBeanMethods = false)
3753
public class WebClientConfig {
3854

3955
@Bean
@@ -50,14 +66,28 @@ public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager
5066
@Bean
5167
public OAuth2AuthorizedClientManager authorizedClientManager(
5268
ClientRegistrationRepository clientRegistrationRepository,
53-
OAuth2AuthorizedClientRepository authorizedClientRepository) {
69+
OAuth2AuthorizedClientRepository authorizedClientRepository,
70+
RestTemplateBuilder restTemplateBuilder,
71+
Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
72+
73+
// @formatter:off
74+
RestTemplate restTemplate = restTemplateBuilder
75+
.requestFactory(clientHttpRequestFactory)
76+
.messageConverters(Arrays.asList(
77+
new FormHttpMessageConverter(),
78+
new OAuth2AccessTokenResponseHttpMessageConverter()))
79+
.errorHandler(new OAuth2ErrorResponseErrorHandler())
80+
.build();
81+
// @formatter:on
5482

5583
// @formatter:off
5684
OAuth2AuthorizedClientProvider authorizedClientProvider =
5785
OAuth2AuthorizedClientProviderBuilder.builder()
5886
.authorizationCode()
5987
.refreshToken()
60-
.clientCredentials()
88+
.clientCredentials(clientCredentials ->
89+
clientCredentials.accessTokenResponseClient(
90+
createClientCredentialsTokenResponseClient(restTemplate)))
6191
.provider(new DeviceCodeOAuth2AuthorizedClientProvider())
6292
.build();
6393
// @formatter:on
@@ -73,4 +103,23 @@ public OAuth2AuthorizedClientManager authorizedClientManager(
73103
return authorizedClientManager;
74104
}
75105

106+
private static OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> createClientCredentialsTokenResponseClient(
107+
RestTemplate restTemplate) {
108+
DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
109+
new DefaultClientCredentialsTokenResponseClient();
110+
clientCredentialsTokenResponseClient.setRestOperations(restTemplate);
111+
112+
OAuth2ClientCredentialsGrantRequestEntityConverter clientCredentialsGrantRequestEntityConverter =
113+
new OAuth2ClientCredentialsGrantRequestEntityConverter();
114+
clientCredentialsGrantRequestEntityConverter.addParametersConverter(authorizationGrantRequest -> {
115+
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
116+
// client_id parameter is required for tls_client_auth method
117+
parameters.add(OAuth2ParameterNames.CLIENT_ID, authorizationGrantRequest.getClientRegistration().getClientId());
118+
return parameters;
119+
});
120+
clientCredentialsTokenResponseClient.setRequestEntityConverter(clientCredentialsGrantRequestEntityConverter);
121+
122+
return clientCredentialsTokenResponseClient;
123+
}
124+
76125
}

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ public String authorizationFailed(Model model, HttpServletRequest request) {
8484
return "index";
8585
}
8686

87-
@GetMapping(value = "/authorize", params = "grant_type=client_credentials")
88-
public String clientCredentialsGrant(Model model) {
87+
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=client_secret"})
88+
public String clientCredentialsGrantUsingClientSecret(Model model) {
8989

9090
String[] messages = this.webClient
9191
.get()
@@ -99,6 +99,21 @@ public String clientCredentialsGrant(Model model) {
9999
return "index";
100100
}
101101

102+
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=mtls"})
103+
public String clientCredentialsGrantUsingMutualTLS(Model model) {
104+
105+
String[] messages = this.webClient
106+
.get()
107+
.uri(this.messagesBaseUri)
108+
.attributes(clientRegistrationId("mtls-demo-client-client-credentials"))
109+
.retrieve()
110+
.bodyToMono(String[].class)
111+
.block();
112+
model.addAttribute("messages", messages);
113+
114+
return "index";
115+
}
116+
102117
@GetMapping(value = "/authorize", params = "grant_type=token_exchange")
103118
public String tokenExchangeGrant(Model model) {
104119

samples/demo-client/src/main/resources/application.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@ logging:
99
org.springframework.security.oauth2: INFO
1010

1111
spring:
12+
ssl:
13+
bundle:
14+
jks:
15+
demo-client:
16+
key:
17+
alias: demo-client-sample
18+
password: password
19+
keystore:
20+
location: classpath:keystore.p12
21+
password: password
22+
type: PKCS12
23+
truststore:
24+
location: classpath:keystore.p12
25+
password: password
26+
type: PKCS12
1227
thymeleaf:
1328
cache: false
1429
security:
@@ -53,9 +68,18 @@ spring:
5368
authorization-grant-type: urn:ietf:params:oauth:grant-type:device_code
5469
scope: message.read,message.write
5570
client-name: messaging-client-device-code
71+
mtls-demo-client-client-credentials:
72+
provider: spring-tls
73+
client-id: mtls-demo-client
74+
client-authentication-method: tls_client_auth
75+
authorization-grant-type: client_credentials
76+
scope: message.read,message.write
77+
client-name: mtls-demo-client-client-credentials
5678
provider:
5779
spring:
5880
issuer-uri: http://localhost:9000
81+
spring-tls:
82+
token-uri: https://localhost:9443/oauth2/token
5983

6084
messages:
6185
base-uri: http://127.0.0.1:8090/messages

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Authorize</a>
2525
<ul class="dropdown-menu">
2626
<li><a class="dropdown-item" href="/authorize?grant_type=authorization_code" th:href="@{/authorize?grant_type=authorization_code}">Authorization Code</a></li>
27-
<li><a class="dropdown-item" href="/authorize?grant_type=client_credentials" th:href="@{/authorize?grant_type=client_credentials}">Client Credentials</a></li>
27+
<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>
28+
<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>
2829
<li><a class="dropdown-item" href="/authorize?grant_type=token_exchange" th:href="@{/authorize?grant_type=token_exchange}">Token Exchange</a></li>
2930
<li><a class="dropdown-item" href="/authorize?grant_type=device_code" th:href="@{/authorize?grant_type=device_code}">Device Code</a></li>
3031
</ul>

0 commit comments

Comments
 (0)