From ff4b542b83c5ed552d3303264a1d4293cbd5ab7d Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 12 Apr 2024 09:01:19 -0400 Subject: [PATCH] Configure sample with Mutual-TLS client certificate-bound access tokens Issue gh-1560 --- .../config/AuthorizationServerConfig.java | 6 +++ .../java/sample/config/WebClientConfig.java | 37 ++++++++++++++- .../src/main/resources/application.yml | 2 +- .../sample/config/ResourceServerConfig.java | 8 ++++ .../sample/config/TomcatServerConfig.java | 46 +++++++++++++++++++ .../src/main/resources/application.yml | 20 +++++++- 6 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 samples/messages-resource/src/main/java/sample/config/TomcatServerConfig.java diff --git a/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java b/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java index 7cdd3dbd..0608737f 100644 --- a/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java +++ b/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -50,6 +50,7 @@ import org.springframework.security.oauth2.server.authorization.config.annotatio import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; import org.springframework.security.web.SecurityFilterChain; @@ -179,6 +180,11 @@ public class AuthorizationServerConfig { .jwkSetUrl("http://127.0.0.1:8080/jwks") .build() ) + .tokenSettings( + TokenSettings.builder() + .x509CertificateBoundAccessTokens(true) + .build() + ) .build(); // Save registered client's in db as if in-memory diff --git a/samples/demo-client/src/main/java/sample/config/WebClientConfig.java b/samples/demo-client/src/main/java/sample/config/WebClientConfig.java index e00fe8ee..8722d8c8 100644 --- a/samples/demo-client/src/main/java/sample/config/WebClientConfig.java +++ b/samples/demo-client/src/main/java/sample/config/WebClientConfig.java @@ -18,13 +18,24 @@ package sample.config; import java.util.Arrays; import java.util.function.Supplier; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import reactor.netty.http.client.HttpClient; +import reactor.netty.tcp.SslProvider; import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; 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.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; @@ -54,11 +65,15 @@ import org.springframework.web.reactive.function.client.WebClient; public class WebClientConfig { @Bean("default-client-web-client") - public WebClient defaultClientWebClient(OAuth2AuthorizedClientManager authorizedClientManager) { + public WebClient defaultClientWebClient( + OAuth2AuthorizedClientManager authorizedClientManager, + SslBundles sslBundles) throws Exception { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); // @formatter:off return WebClient.builder() + .clientConnector(createClientConnector(sslBundles.getBundle("demo-client"))) .apply(oauth2Client.oauth2Configuration()) .build(); // @formatter:on @@ -69,7 +84,8 @@ public class WebClientConfig { ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository, RestTemplateBuilder restTemplateBuilder, - @Qualifier("self-signed-demo-client-http-request-factory") Supplier clientHttpRequestFactory) { + @Qualifier("self-signed-demo-client-http-request-factory") Supplier clientHttpRequestFactory, + SslBundles sslBundles) throws Exception { // @formatter:off RestTemplate restTemplate = restTemplateBuilder @@ -98,6 +114,7 @@ public class WebClientConfig { new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); // @formatter:off return WebClient.builder() + .clientConnector(createClientConnector(sslBundles.getBundle("self-signed-demo-client"))) .apply(oauth2Client.oauth2Configuration()) .build(); // @formatter:on @@ -143,6 +160,22 @@ public class WebClientConfig { return authorizedClientManager; } + private static ClientHttpConnector createClientConnector(SslBundle sslBundle) throws Exception { + KeyManagerFactory keyManagerFactory = sslBundle.getManagers().getKeyManagerFactory(); + TrustManagerFactory trustManagerFactory = sslBundle.getManagers().getTrustManagerFactory(); + + // @formatter:off + SslContext sslContext = SslContextBuilder.forClient() + .keyManager(keyManagerFactory) + .trustManager(trustManagerFactory) + .build(); + // @formatter:on + + SslProvider sslProvider = SslProvider.builder().sslContext(sslContext).build(); + HttpClient httpClient = HttpClient.create().secure(sslProvider); + return new ReactorClientHttpConnector(httpClient); + } + private static OAuth2AccessTokenResponseClient createClientCredentialsTokenResponseClient( RestTemplate restTemplate) { DefaultClientCredentialsTokenResponseClient clientCredentialsTokenResponseClient = diff --git a/samples/demo-client/src/main/resources/application.yml b/samples/demo-client/src/main/resources/application.yml index 89f034f7..7ab6d2d1 100644 --- a/samples/demo-client/src/main/resources/application.yml +++ b/samples/demo-client/src/main/resources/application.yml @@ -101,7 +101,7 @@ spring: token-uri: https://localhost:9443/oauth2/token messages: - base-uri: http://127.0.0.1:8090/messages + base-uri: https://127.0.0.1:8443/messages user-messages: base-uri: http://127.0.0.1:8091/user/messages diff --git a/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java b/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java index 08ec09c5..d5a8ce7d 100644 --- a/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java +++ b/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java @@ -30,6 +30,14 @@ import org.springframework.security.web.SecurityFilterChain; @Configuration(proxyBeanMethods = false) public class ResourceServerConfig { + /* + NOTE: + The `NimbusJwtDecoder` `@Bean` autoconfigured by Spring Boot will contain + an `OAuth2TokenValidator` of type `X509CertificateThumbprintValidator`. + This is the validator responsible for validating the `x5t#S256` claim (if available) + in the `Jwt` against the SHA-256 Thumbprint of the supplied `X509Certificate`. + */ + // @formatter:off @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { diff --git a/samples/messages-resource/src/main/java/sample/config/TomcatServerConfig.java b/samples/messages-resource/src/main/java/sample/config/TomcatServerConfig.java new file mode 100644 index 00000000..54121986 --- /dev/null +++ b/samples/messages-resource/src/main/java/sample/config/TomcatServerConfig.java @@ -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 connectorCustomizer() { + return (tomcat) -> tomcat.addAdditionalTomcatConnectors(createHttpConnector()); + } + + private Connector createHttpConnector() { + Connector connector = new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); + connector.setScheme("http"); + connector.setPort(8090); + connector.setSecure(false); + connector.setRedirectPort(8443); + return connector; + } + +} diff --git a/samples/messages-resource/src/main/resources/application.yml b/samples/messages-resource/src/main/resources/application.yml index 5a78b0db..8634d16b 100644 --- a/samples/messages-resource/src/main/resources/application.yml +++ b/samples/messages-resource/src/main/resources/application.yml @@ -1,5 +1,8 @@ server: - port: 8090 + port: 8443 + ssl: + bundle: messages-resource + client-auth: need logging: level: @@ -10,6 +13,21 @@ logging: # org.springframework.boot.autoconfigure: DEBUG spring: + ssl: + bundle: + jks: + messages-resource: + key: + alias: messages-resource-sample + password: password + keystore: + location: classpath:keystore.p12 + password: password + type: PKCS12 + truststore: + location: classpath:keystore.p12 + password: password + type: PKCS12 security: oauth2: resourceserver: