Skip to content

Commit 9bd0043

Browse files
committed
Add sample for self-signed certificate Mutual-TLS client authentication method
Issue gh-1559
1 parent 31e97c8 commit 9bd0043

File tree

9 files changed

+188
-16
lines changed

9 files changed

+188
-16
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,14 @@ public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jd
169169
RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString())
170170
.clientId("mtls-demo-client")
171171
.clientAuthenticationMethod(new ClientAuthenticationMethod("tls_client_auth"))
172+
.clientAuthenticationMethod(new ClientAuthenticationMethod("self_signed_tls_client_auth"))
172173
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
173174
.scope("message.read")
174175
.scope("message.write")
175176
.clientSettings(
176177
ClientSettings.builder()
177178
.x509CertificateSubjectDN("CN=demo-client-sample,OU=Spring Samples,O=Spring,C=US")
179+
.jwkSetUrl("http://127.0.0.1:8080/jwks")
178180
.build()
179181
)
180182
.build();

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
@Configuration(proxyBeanMethods = false)
4444
public class RestTemplateConfig {
4545

46-
@Bean
47-
Supplier<ClientHttpRequestFactory> clientHttpRequestFactory(SslBundles sslBundles) {
46+
@Bean("default-client-http-request-factory")
47+
Supplier<ClientHttpRequestFactory> defaultClientHttpRequestFactory(SslBundles sslBundles) {
4848
return () -> {
4949
SslBundle sslBundle = sslBundles.getBundle("demo-client");
5050
final SSLContext sslContext = sslBundle.createSslContext();
@@ -63,4 +63,23 @@ Supplier<ClientHttpRequestFactory> clientHttpRequestFactory(SslBundles sslBundle
6363
};
6464
}
6565

66+
@Bean("self-signed-demo-client-http-request-factory")
67+
Supplier<ClientHttpRequestFactory> selfSignedDemoClientHttpRequestFactory(SslBundles sslBundles) {
68+
return () -> {
69+
SslBundle sslBundle = sslBundles.getBundle("self-signed-demo-client");
70+
final SSLContext sslContext = sslBundle.createSslContext();
71+
final SSLConnectionSocketFactory sslConnectionSocketFactory =
72+
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
73+
final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
74+
.register("https", sslConnectionSocketFactory)
75+
.build();
76+
final BasicHttpClientConnectionManager connectionManager =
77+
new BasicHttpClientConnectionManager(socketFactoryRegistry);
78+
final CloseableHttpClient httpClient = HttpClients.custom()
79+
.setConnectionManager(connectionManager)
80+
.build();
81+
return new HttpComponentsClientHttpRequestFactory(httpClient);
82+
};
83+
}
84+
6685
}

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

Lines changed: 2 additions & 2 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.
@@ -49,7 +49,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http,
4949
http
5050
.authorizeHttpRequests(authorize ->
5151
authorize
52-
.requestMatchers("/logged-out").permitAll()
52+
.requestMatchers("/jwks", "/logged-out").permitAll()
5353
.anyRequest().authenticated()
5454
)
5555
.oauth2Login(oauth2Login ->

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

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider;
2222

23+
import org.springframework.beans.factory.annotation.Qualifier;
2324
import org.springframework.boot.web.client.RestTemplateBuilder;
2425
import org.springframework.context.annotation.Bean;
2526
import org.springframework.context.annotation.Configuration;
@@ -52,8 +53,47 @@
5253
@Configuration(proxyBeanMethods = false)
5354
public class WebClientConfig {
5455

55-
@Bean
56-
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
56+
@Bean("default-client-web-client")
57+
public WebClient defaultClientWebClient(OAuth2AuthorizedClientManager authorizedClientManager) {
58+
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
59+
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
60+
// @formatter:off
61+
return WebClient.builder()
62+
.apply(oauth2Client.oauth2Configuration())
63+
.build();
64+
// @formatter:on
65+
}
66+
67+
@Bean("self-signed-demo-client-web-client")
68+
public WebClient selfSignedDemoClientWebClient(
69+
ClientRegistrationRepository clientRegistrationRepository,
70+
OAuth2AuthorizedClientRepository authorizedClientRepository,
71+
RestTemplateBuilder restTemplateBuilder,
72+
@Qualifier("self-signed-demo-client-http-request-factory") Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
73+
74+
// @formatter:off
75+
RestTemplate restTemplate = restTemplateBuilder
76+
.requestFactory(clientHttpRequestFactory)
77+
.messageConverters(Arrays.asList(
78+
new FormHttpMessageConverter(),
79+
new OAuth2AccessTokenResponseHttpMessageConverter()))
80+
.errorHandler(new OAuth2ErrorResponseErrorHandler())
81+
.build();
82+
// @formatter:on
83+
84+
// @formatter:off
85+
OAuth2AuthorizedClientProvider authorizedClientProvider =
86+
OAuth2AuthorizedClientProviderBuilder.builder()
87+
.clientCredentials(clientCredentials ->
88+
clientCredentials.accessTokenResponseClient(
89+
createClientCredentialsTokenResponseClient(restTemplate)))
90+
.build();
91+
// @formatter:on
92+
93+
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
94+
clientRegistrationRepository, authorizedClientRepository);
95+
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
96+
5797
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
5898
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
5999
// @formatter:off
@@ -68,7 +108,7 @@ public OAuth2AuthorizedClientManager authorizedClientManager(
68108
ClientRegistrationRepository clientRegistrationRepository,
69109
OAuth2AuthorizedClientRepository authorizedClientRepository,
70110
RestTemplateBuilder restTemplateBuilder,
71-
Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
111+
@Qualifier("default-client-http-request-factory") Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
72112

73113
// @formatter:off
74114
RestTemplate restTemplate = restTemplateBuilder

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import jakarta.servlet.http.HttpServletRequest;
1919

20+
import org.springframework.beans.factory.annotation.Qualifier;
2021
import org.springframework.beans.factory.annotation.Value;
2122
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
2223
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
@@ -39,14 +40,18 @@
3940
*/
4041
@Controller
4142
public class AuthorizationController {
42-
private final WebClient webClient;
43+
private final WebClient defaultClientWebClient;
44+
private final WebClient selfSignedDemoClientWebClient;
4345
private final String messagesBaseUri;
4446
private final String userMessagesBaseUri;
4547

46-
public AuthorizationController(WebClient webClient,
48+
public AuthorizationController(
49+
@Qualifier("default-client-web-client") WebClient defaultClientWebClient,
50+
@Qualifier("self-signed-demo-client-web-client") WebClient selfSignedDemoClientWebClient,
4751
@Value("${messages.base-uri}") String messagesBaseUri,
4852
@Value("${user-messages.base-uri}") String userMessagesBaseUri) {
49-
this.webClient = webClient;
53+
this.defaultClientWebClient = defaultClientWebClient;
54+
this.selfSignedDemoClientWebClient = selfSignedDemoClientWebClient;
5055
this.messagesBaseUri = messagesBaseUri;
5156
this.userMessagesBaseUri = userMessagesBaseUri;
5257
}
@@ -56,7 +61,7 @@ public String authorizationCodeGrant(Model model,
5661
@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code")
5762
OAuth2AuthorizedClient authorizedClient) {
5863

59-
String[] messages = this.webClient
64+
String[] messages = this.defaultClientWebClient
6065
.get()
6166
.uri(this.messagesBaseUri)
6267
.attributes(oauth2AuthorizedClient(authorizedClient))
@@ -87,7 +92,7 @@ public String authorizationFailed(Model model, HttpServletRequest request) {
8792
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=client_secret"})
8893
public String clientCredentialsGrantUsingClientSecret(Model model) {
8994

90-
String[] messages = this.webClient
95+
String[] messages = this.defaultClientWebClient
9196
.get()
9297
.uri(this.messagesBaseUri)
9398
.attributes(clientRegistrationId("messaging-client-client-credentials"))
@@ -102,7 +107,7 @@ public String clientCredentialsGrantUsingClientSecret(Model model) {
102107
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=mtls"})
103108
public String clientCredentialsGrantUsingMutualTLS(Model model) {
104109

105-
String[] messages = this.webClient
110+
String[] messages = this.defaultClientWebClient
106111
.get()
107112
.uri(this.messagesBaseUri)
108113
.attributes(clientRegistrationId("mtls-demo-client-client-credentials"))
@@ -114,10 +119,25 @@ public String clientCredentialsGrantUsingMutualTLS(Model model) {
114119
return "index";
115120
}
116121

122+
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=self_signed_mtls"})
123+
public String clientCredentialsGrantUsingSelfSignedMutualTLS(Model model) {
124+
125+
String[] messages = this.selfSignedDemoClientWebClient
126+
.get()
127+
.uri(this.messagesBaseUri)
128+
.attributes(clientRegistrationId("mtls-self-signed-demo-client-client-credentials"))
129+
.retrieve()
130+
.bodyToMono(String[].class)
131+
.block();
132+
model.addAttribute("messages", messages);
133+
134+
return "index";
135+
}
136+
117137
@GetMapping(value = "/authorize", params = "grant_type=token_exchange")
118138
public String tokenExchangeGrant(Model model) {
119139

120-
String[] messages = this.webClient
140+
String[] messages = this.defaultClientWebClient
121141
.get()
122142
.uri(this.userMessagesBaseUri)
123143
.attributes(clientRegistrationId("user-client-authorization-code"))

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

Lines changed: 5 additions & 2 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.
@@ -22,6 +22,7 @@
2222
import java.util.Objects;
2323
import java.util.Set;
2424

25+
import org.springframework.beans.factory.annotation.Qualifier;
2526
import org.springframework.beans.factory.annotation.Value;
2627
import org.springframework.core.ParameterizedTypeReference;
2728
import org.springframework.http.HttpStatus;
@@ -72,7 +73,9 @@ public class DeviceController {
7273

7374
private final String messagesBaseUri;
7475

75-
public DeviceController(ClientRegistrationRepository clientRegistrationRepository, WebClient webClient,
76+
public DeviceController(
77+
ClientRegistrationRepository clientRegistrationRepository,
78+
@Qualifier("default-client-web-client") WebClient webClient,
7679
@Value("${messages.base-uri}") String messagesBaseUri) {
7780

7881
this.clientRegistrationRepository = clientRegistrationRepository;
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.web;
17+
18+
import java.security.KeyStore;
19+
import java.security.cert.Certificate;
20+
import java.security.interfaces.RSAPublicKey;
21+
import java.util.Collections;
22+
import java.util.Map;
23+
import java.util.UUID;
24+
25+
import com.nimbusds.jose.jwk.JWKSet;
26+
import com.nimbusds.jose.jwk.KeyUse;
27+
import com.nimbusds.jose.jwk.RSAKey;
28+
import com.nimbusds.jose.util.Base64;
29+
30+
import org.springframework.boot.ssl.SslBundle;
31+
import org.springframework.boot.ssl.SslBundles;
32+
import org.springframework.web.bind.annotation.GetMapping;
33+
import org.springframework.web.bind.annotation.RestController;
34+
35+
/**
36+
* @author Joe Grandja
37+
* @since 1.3
38+
*/
39+
@RestController
40+
public class JwkSetController {
41+
private final JWKSet jwkSet;
42+
43+
public JwkSetController(SslBundles sslBundles) throws Exception {
44+
this.jwkSet = initJwkSet(sslBundles);
45+
}
46+
47+
@GetMapping("/jwks")
48+
public Map<String, Object> getJwkSet() {
49+
return this.jwkSet.toJSONObject();
50+
}
51+
52+
private static JWKSet initJwkSet(SslBundles sslBundles) throws Exception {
53+
SslBundle sslBundle = sslBundles.getBundle("self-signed-demo-client");
54+
KeyStore keyStore = sslBundle.getStores().getKeyStore();
55+
String alias = sslBundle.getKey().getAlias();
56+
57+
Certificate certificate = keyStore.getCertificate(alias);
58+
59+
RSAKey rsaKey = new RSAKey.Builder((RSAPublicKey) certificate.getPublicKey())
60+
.keyUse(KeyUse.SIGNATURE)
61+
.keyID(UUID.randomUUID().toString())
62+
.x509CertChain(Collections.singletonList(Base64.encode(certificate.getEncoded())))
63+
.build();
64+
65+
return new JWKSet(rsaKey);
66+
}
67+
68+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ spring:
2424
location: classpath:keystore.p12
2525
password: password
2626
type: PKCS12
27+
self-signed-demo-client:
28+
key:
29+
alias: self-signed-demo-client-sample
30+
password: password
31+
keystore:
32+
location: classpath:keystore-self-signed.p12
33+
password: password
34+
type: PKCS12
35+
truststore:
36+
location: classpath:keystore-self-signed.p12
37+
password: password
38+
type: PKCS12
2739
thymeleaf:
2840
cache: false
2941
security:
@@ -75,6 +87,13 @@ spring:
7587
authorization-grant-type: client_credentials
7688
scope: message.read,message.write
7789
client-name: mtls-demo-client-client-credentials
90+
mtls-self-signed-demo-client-client-credentials:
91+
provider: spring-tls
92+
client-id: mtls-demo-client
93+
client-authentication-method: self_signed_tls_client_auth
94+
authorization-grant-type: client_credentials
95+
scope: message.read,message.write
96+
client-name: mtls-self-signed-demo-client-client-credentials
7897
provider:
7998
spring:
8099
issuer-uri: http://localhost:9000

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<li><a class="dropdown-item" href="/authorize?grant_type=authorization_code" th:href="@{/authorize?grant_type=authorization_code}">Authorization Code</a></li>
2727
<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>
2828
<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>
29+
<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>
2930
<li><a class="dropdown-item" href="/authorize?grant_type=token_exchange" th:href="@{/authorize?grant_type=token_exchange}">Token Exchange</a></li>
3031
<li><a class="dropdown-item" href="/authorize?grant_type=device_code" th:href="@{/authorize?grant_type=device_code}">Device Code</a></li>
3132
</ul>

0 commit comments

Comments
 (0)