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

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