Browse Source

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

Issue gh-1559
pull/1609/head
Joe Grandja 2 years ago
parent
commit
9bd0043cc6
  1. 2
      samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java
  2. 23
      samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java
  3. 4
      samples/demo-client/src/main/java/sample/config/SecurityConfig.java
  4. 46
      samples/demo-client/src/main/java/sample/config/WebClientConfig.java
  5. 34
      samples/demo-client/src/main/java/sample/web/AuthorizationController.java
  6. 7
      samples/demo-client/src/main/java/sample/web/DeviceController.java
  7. 68
      samples/demo-client/src/main/java/sample/web/JwkSetController.java
  8. 19
      samples/demo-client/src/main/resources/application.yml
  9. 1
      samples/demo-client/src/main/resources/templates/page-templates.html

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

@ -169,12 +169,14 @@ public class AuthorizationServerConfig {
RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString()) RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("mtls-demo-client") .clientId("mtls-demo-client")
.clientAuthenticationMethod(new ClientAuthenticationMethod("tls_client_auth")) .clientAuthenticationMethod(new ClientAuthenticationMethod("tls_client_auth"))
.clientAuthenticationMethod(new ClientAuthenticationMethod("self_signed_tls_client_auth"))
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.scope("message.read") .scope("message.read")
.scope("message.write") .scope("message.write")
.clientSettings( .clientSettings(
ClientSettings.builder() ClientSettings.builder()
.x509CertificateSubjectDN("CN=demo-client-sample,OU=Spring Samples,O=Spring,C=US") .x509CertificateSubjectDN("CN=demo-client-sample,OU=Spring Samples,O=Spring,C=US")
.jwkSetUrl("http://127.0.0.1:8080/jwks")
.build() .build()
) )
.build(); .build();

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

@ -43,8 +43,8 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
public class RestTemplateConfig { public class RestTemplateConfig {
@Bean @Bean("default-client-http-request-factory")
Supplier<ClientHttpRequestFactory> clientHttpRequestFactory(SslBundles sslBundles) { Supplier<ClientHttpRequestFactory> defaultClientHttpRequestFactory(SslBundles sslBundles) {
return () -> { return () -> {
SslBundle sslBundle = sslBundles.getBundle("demo-client"); SslBundle sslBundle = sslBundles.getBundle("demo-client");
final SSLContext sslContext = sslBundle.createSslContext(); 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);
};
}
} }

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

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -49,7 +49,7 @@ public class SecurityConfig {
http http
.authorizeHttpRequests(authorize -> .authorizeHttpRequests(authorize ->
authorize authorize
.requestMatchers("/logged-out").permitAll() .requestMatchers("/jwks", "/logged-out").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.oauth2Login(oauth2Login -> .oauth2Login(oauth2Login ->

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

@ -20,6 +20,7 @@ import java.util.function.Supplier;
import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider; import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -52,8 +53,47 @@ import org.springframework.web.reactive.function.client.WebClient;
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
public class WebClientConfig { public class WebClientConfig {
@Bean @Bean("default-client-web-client")
public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { 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 = ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
// @formatter:off // @formatter:off
@ -68,7 +108,7 @@ public class WebClientConfig {
ClientRegistrationRepository clientRegistrationRepository, ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository, OAuth2AuthorizedClientRepository authorizedClientRepository,
RestTemplateBuilder restTemplateBuilder, RestTemplateBuilder restTemplateBuilder,
Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) { @Qualifier("default-client-http-request-factory") Supplier<ClientHttpRequestFactory> clientHttpRequestFactory) {
// @formatter:off // @formatter:off
RestTemplate restTemplate = restTemplateBuilder RestTemplate restTemplate = restTemplateBuilder

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

@ -17,6 +17,7 @@ package sample.web;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
@ -39,14 +40,18 @@ import static org.springframework.security.oauth2.client.web.reactive.function.c
*/ */
@Controller @Controller
public class AuthorizationController { public class AuthorizationController {
private final WebClient webClient; private final WebClient defaultClientWebClient;
private final WebClient selfSignedDemoClientWebClient;
private final String messagesBaseUri; private final String messagesBaseUri;
private final String userMessagesBaseUri; 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("${messages.base-uri}") String messagesBaseUri,
@Value("${user-messages.base-uri}") String userMessagesBaseUri) { @Value("${user-messages.base-uri}") String userMessagesBaseUri) {
this.webClient = webClient; this.defaultClientWebClient = defaultClientWebClient;
this.selfSignedDemoClientWebClient = selfSignedDemoClientWebClient;
this.messagesBaseUri = messagesBaseUri; this.messagesBaseUri = messagesBaseUri;
this.userMessagesBaseUri = userMessagesBaseUri; this.userMessagesBaseUri = userMessagesBaseUri;
} }
@ -56,7 +61,7 @@ public class AuthorizationController {
@RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") @RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code")
OAuth2AuthorizedClient authorizedClient) { OAuth2AuthorizedClient authorizedClient) {
String[] messages = this.webClient String[] messages = this.defaultClientWebClient
.get() .get()
.uri(this.messagesBaseUri) .uri(this.messagesBaseUri)
.attributes(oauth2AuthorizedClient(authorizedClient)) .attributes(oauth2AuthorizedClient(authorizedClient))
@ -87,7 +92,7 @@ public class AuthorizationController {
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=client_secret"}) @GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=client_secret"})
public String clientCredentialsGrantUsingClientSecret(Model model) { public String clientCredentialsGrantUsingClientSecret(Model model) {
String[] messages = this.webClient String[] messages = this.defaultClientWebClient
.get() .get()
.uri(this.messagesBaseUri) .uri(this.messagesBaseUri)
.attributes(clientRegistrationId("messaging-client-client-credentials")) .attributes(clientRegistrationId("messaging-client-client-credentials"))
@ -102,7 +107,7 @@ public class AuthorizationController {
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=mtls"}) @GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=mtls"})
public String clientCredentialsGrantUsingMutualTLS(Model model) { public String clientCredentialsGrantUsingMutualTLS(Model model) {
String[] messages = this.webClient String[] messages = this.defaultClientWebClient
.get() .get()
.uri(this.messagesBaseUri) .uri(this.messagesBaseUri)
.attributes(clientRegistrationId("mtls-demo-client-client-credentials")) .attributes(clientRegistrationId("mtls-demo-client-client-credentials"))
@ -114,10 +119,25 @@ public class AuthorizationController {
return "index"; 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") @GetMapping(value = "/authorize", params = "grant_type=token_exchange")
public String tokenExchangeGrant(Model model) { public String tokenExchangeGrant(Model model) {
String[] messages = this.webClient String[] messages = this.defaultClientWebClient
.get() .get()
.uri(this.userMessagesBaseUri) .uri(this.userMessagesBaseUri)
.attributes(clientRegistrationId("user-client-authorization-code")) .attributes(clientRegistrationId("user-client-authorization-code"))

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

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Objects;
import java.util.Set; import java.util.Set;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@ -72,7 +73,9 @@ public class DeviceController {
private final String messagesBaseUri; 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) { @Value("${messages.base-uri}") String messagesBaseUri) {
this.clientRegistrationRepository = clientRegistrationRepository; this.clientRegistrationRepository = clientRegistrationRepository;

68
samples/demo-client/src/main/java/sample/web/JwkSetController.java

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

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

@ -24,6 +24,18 @@ spring:
location: classpath:keystore.p12 location: classpath:keystore.p12
password: password password: password
type: PKCS12 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: thymeleaf:
cache: false cache: false
security: security:
@ -75,6 +87,13 @@ spring:
authorization-grant-type: client_credentials authorization-grant-type: client_credentials
scope: message.read,message.write scope: message.read,message.write
client-name: mtls-demo-client-client-credentials 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: provider:
spring: spring:
issuer-uri: http://localhost:9000 issuer-uri: http://localhost:9000

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

@ -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=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=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=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=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> <li><a class="dropdown-item" href="/authorize?grant_type=device_code" th:href="@{/authorize?grant_type=device_code}">Device Code</a></li>
</ul> </ul>

Loading…
Cancel
Save