Add sample for PKI Mutual-TLS client authentication method
Issue gh-1558
This commit is contained in:
@@ -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 {
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 @@
|
||||
*/
|
||||
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;
|
||||
* @author Steve Riesenberg
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Configuration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
@@ -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 {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
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) {
|
||||
|
||||
|
||||
@@ -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:
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
@@ -14,4 +14,4 @@ spring:
|
||||
oauth2:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: http://localhost:9000
|
||||
jwk-set-uri: http://localhost:9000/oauth2/jwks
|
||||
|
||||
Reference in New Issue
Block a user