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

Issue gh-1559
This commit is contained in:
Joe Grandja
2024-04-02 15:38:43 -04:00
parent 31e97c8113
commit 9bd0043cc6
9 changed files with 188 additions and 16 deletions

View File

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

View File

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

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.
@@ -49,7 +49,7 @@ public class SecurityConfig {
http
.authorizeHttpRequests(authorize ->
authorize
.requestMatchers("/logged-out").permitAll()
.requestMatchers("/jwks", "/logged-out").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2Login ->

View File

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

View File

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

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

View File

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

View File

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

View File

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