Browse Source

Add sample for PKI Mutual-TLS client authentication method

Issue gh-1558
pull/1578/head
Joe Grandja 2 years ago
parent
commit
d6a87532a9
  1. 18
      samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java
  2. 46
      samples/demo-authorizationserver/src/main/java/sample/config/TomcatServerConfig.java
  3. 20
      samples/demo-authorizationserver/src/main/resources/application.yml
  4. 1
      samples/demo-client/samples-demo-client.gradle
  5. 66
      samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java
  6. 57
      samples/demo-client/src/main/java/sample/config/WebClientConfig.java
  7. 19
      samples/demo-client/src/main/java/sample/web/AuthorizationController.java
  8. 24
      samples/demo-client/src/main/resources/application.yml
  9. 3
      samples/demo-client/src/main/resources/templates/page-templates.html
  10. 14
      samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java
  11. 2
      samples/messages-resource/src/main/resources/application.yml

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

@ -131,7 +131,7 @@ public class AuthorizationServerConfig { @@ -131,7 +131,7 @@ public class AuthorizationServerConfig {
// @formatter:off
@Bean
public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("messaging-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
@ -166,11 +166,25 @@ public class AuthorizationServerConfig { @@ -166,11 +166,25 @@ public class AuthorizationServerConfig {
.scope("message.read")
.build();
RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("mtls-demo-client")
.clientAuthenticationMethod(new ClientAuthenticationMethod("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")
.build()
)
.build();
// Save registered client's in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
registeredClientRepository.save(registeredClient);
registeredClientRepository.save(messagingClient);
registeredClientRepository.save(deviceClient);
registeredClientRepository.save(tokenExchangeClient);
registeredClientRepository.save(mtlsDemoClient);
return registeredClientRepository;
}

46
samples/demo-authorizationserver/src/main/java/sample/config/TomcatServerConfig.java

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
/*
* 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.config;
import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Joe Grandja
* @since 1.3
*/
@Configuration(proxyBeanMethods = false)
public class TomcatServerConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> connectorCustomizer() {
return (tomcat) -> tomcat.addAdditionalTomcatConnectors(createHttpConnector());
}
private Connector createHttpConnector() {
Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL);
connector.setScheme("http");
connector.setPort(9000);
connector.setSecure(false);
connector.setRedirectPort(9443);
return connector;
}
}

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

@ -1,7 +1,25 @@ @@ -1,7 +1,25 @@
server:
port: 9000
port: 9443
ssl:
bundle: demo-authorizationserver
client-auth: want
spring:
ssl:
bundle:
jks:
demo-authorizationserver:
key:
alias: demo-authorizationserver-sample
password: password
keystore:
location: classpath:keystore.p12
password: password
type: PKCS12
truststore:
location: classpath:keystore.p12
password: password
type: PKCS12
security:
oauth2:
client:

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

@ -23,6 +23,7 @@ dependencies { @@ -23,6 +23,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation "org.springframework:spring-webflux"
implementation "io.projectreactor.netty:reactor-netty"
implementation "org.apache.httpcomponents.client5:httpclient5"
implementation "org.webjars:webjars-locator-core"
implementation "org.webjars:bootstrap:5.2.3"
implementation "org.webjars:popper.js:2.9.3"

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

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* 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.config;
import java.util.function.Supplier;
import javax.net.ssl.SSLContext;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
/**
* @author Joe Grandja
* @since 1.3
*/
@Configuration(proxyBeanMethods = false)
public class RestTemplateConfig {
@Bean
Supplier<ClientHttpRequestFactory> clientHttpRequestFactory(SslBundles sslBundles) {
return () -> {
SslBundle sslBundle = sslBundles.getBundle("demo-client");
final SSLContext sslContext = sslBundle.createSslContext();
final SSLConnectionSocketFactory sslConnectionSocketFactory =
new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
final Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslConnectionSocketFactory)
.build();
final BasicHttpClientConnectionManager connectionManager =
new BasicHttpClientConnectionManager(socketFactoryRegistry);
final CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.build();
return new HttpComponentsClientHttpRequestFactory(httpClient);
};
}
}

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

@ -1,5 +1,5 @@ @@ -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.
@ -15,17 +15,33 @@ @@ -15,17 +15,33 @@
*/
package sample.config;
import java.util.Arrays;
import java.util.function.Supplier;
import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.endpoint.DefaultClientCredentialsTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequest;
import org.springframework.security.oauth2.client.endpoint.OAuth2ClientCredentialsGrantRequestEntityConverter;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
/**
@ -33,7 +49,7 @@ import org.springframework.web.reactive.function.client.WebClient; @@ -33,7 +49,7 @@ import org.springframework.web.reactive.function.client.WebClient;
* @author Steve Riesenberg
* @since 0.0.1
*/
@Configuration
@Configuration(proxyBeanMethods = false)
public class WebClientConfig {
@Bean
@ -50,14 +66,28 @@ public class WebClientConfig { @@ -50,14 +66,28 @@ public class WebClientConfig {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientRepository authorizedClientRepository,
RestTemplateBuilder restTemplateBuilder,
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()
.authorizationCode()
.refreshToken()
.clientCredentials()
.clientCredentials(clientCredentials ->
clientCredentials.accessTokenResponseClient(
createClientCredentialsTokenResponseClient(restTemplate)))
.provider(new DeviceCodeOAuth2AuthorizedClientProvider())
.build();
// @formatter:on
@ -73,4 +103,23 @@ public class WebClientConfig { @@ -73,4 +103,23 @@ public class WebClientConfig {
return authorizedClientManager;
}
private static OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> createClientCredentialsTokenResponseClient(
RestTemplate restTemplate) {
DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
clientCredentialsTokenResponseClient.setRestOperations(restTemplate);
OAuth2ClientCredentialsGrantRequestEntityConverter clientCredentialsGrantRequestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter();
clientCredentialsGrantRequestEntityConverter.addParametersConverter(authorizationGrantRequest -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
// client_id parameter is required for tls_client_auth method
parameters.add(OAuth2ParameterNames.CLIENT_ID, authorizationGrantRequest.getClientRegistration().getClientId());
return parameters;
});
clientCredentialsTokenResponseClient.setRequestEntityConverter(clientCredentialsGrantRequestEntityConverter);
return clientCredentialsTokenResponseClient;
}
}

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

@ -84,8 +84,8 @@ public class AuthorizationController { @@ -84,8 +84,8 @@ public class AuthorizationController {
return "index";
}
@GetMapping(value = "/authorize", params = "grant_type=client_credentials")
public String clientCredentialsGrant(Model model) {
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=client_secret"})
public String clientCredentialsGrantUsingClientSecret(Model model) {
String[] messages = this.webClient
.get()
@ -99,6 +99,21 @@ public class AuthorizationController { @@ -99,6 +99,21 @@ public class AuthorizationController {
return "index";
}
@GetMapping(value = "/authorize", params = {"grant_type=client_credentials", "client_auth=mtls"})
public String clientCredentialsGrantUsingMutualTLS(Model model) {
String[] messages = this.webClient
.get()
.uri(this.messagesBaseUri)
.attributes(clientRegistrationId("mtls-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) {

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

@ -9,6 +9,21 @@ logging: @@ -9,6 +9,21 @@ logging:
org.springframework.security.oauth2: INFO
spring:
ssl:
bundle:
jks:
demo-client:
key:
alias: demo-client-sample
password: password
keystore:
location: classpath:keystore.p12
password: password
type: PKCS12
truststore:
location: classpath:keystore.p12
password: password
type: PKCS12
thymeleaf:
cache: false
security:
@ -53,9 +68,18 @@ spring: @@ -53,9 +68,18 @@ spring:
authorization-grant-type: urn:ietf:params:oauth:grant-type:device_code
scope: message.read,message.write
client-name: messaging-client-device-code
mtls-demo-client-client-credentials:
provider: spring-tls
client-id: mtls-demo-client
client-authentication-method: tls_client_auth
authorization-grant-type: client_credentials
scope: message.read,message.write
client-name: mtls-demo-client-client-credentials
provider:
spring:
issuer-uri: http://localhost:9000
spring-tls:
token-uri: https://localhost:9443/oauth2/token
messages:
base-uri: http://127.0.0.1:8090/messages

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

@ -24,7 +24,8 @@ @@ -24,7 +24,8 @@
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">Authorize</a>
<ul class="dropdown-menu">
<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" th:href="@{/authorize?grant_type=client_credentials}">Client Credentials</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=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>

14
samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2020-2022 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.
@ -17,6 +17,7 @@ package sample.config; @@ -17,6 +17,7 @@ package sample.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@ -34,11 +35,12 @@ public class ResourceServerConfig { @@ -34,11 +35,12 @@ public class ResourceServerConfig {
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/messages/**")
.authorizeHttpRequests()
.requestMatchers("/messages/**").hasAuthority("SCOPE_message.read")
.and()
.oauth2ResourceServer()
.jwt();
.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/messages/**").hasAuthority("SCOPE_message.read")
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(Customizer.withDefaults())
);
return http.build();
}
// @formatter:on

2
samples/messages-resource/src/main/resources/application.yml

@ -14,4 +14,4 @@ spring: @@ -14,4 +14,4 @@ spring:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:9000
jwk-set-uri: http://localhost:9000/oauth2/jwks

Loading…
Cancel
Save