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 c1049068..eaea8478 100644 --- a/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java +++ b/samples/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -169,12 +169,14 @@ public class AuthorizationServerConfig { RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("mtls-demo-client") .clientAuthenticationMethod(new ClientAuthenticationMethod("tls_client_auth")) + .clientAuthenticationMethod(new ClientAuthenticationMethod("self_signed_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") + .jwkSetUrl("http://127.0.0.1:8080/jwks") .build() ) .build(); diff --git a/samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java b/samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java index 557f2d30..052e4e45 100644 --- a/samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java +++ b/samples/demo-client/src/main/java/sample/config/RestTemplateConfig.java @@ -43,8 +43,8 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; @Configuration(proxyBeanMethods = false) public class RestTemplateConfig { - @Bean - Supplier clientHttpRequestFactory(SslBundles sslBundles) { + @Bean("default-client-http-request-factory") + Supplier 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 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 socketFactoryRegistry = RegistryBuilder.create() + .register("https", sslConnectionSocketFactory) + .build(); + final BasicHttpClientConnectionManager connectionManager = + new BasicHttpClientConnectionManager(socketFactoryRegistry); + final CloseableHttpClient httpClient = HttpClients.custom() + .setConnectionManager(connectionManager) + .build(); + return new HttpComponentsClientHttpRequestFactory(httpClient); + }; + } + } diff --git a/samples/demo-client/src/main/java/sample/config/SecurityConfig.java b/samples/demo-client/src/main/java/sample/config/SecurityConfig.java index 8379f76a..17208892 100644 --- a/samples/demo-client/src/main/java/sample/config/SecurityConfig.java +++ b/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"); * 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 -> 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 5208d7eb..e00fe8ee 100644 --- a/samples/demo-client/src/main/java/sample/config/WebClientConfig.java +++ b/samples/demo-client/src/main/java/sample/config/WebClientConfig.java @@ -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) { + + // @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) { + @Qualifier("default-client-http-request-factory") Supplier clientHttpRequestFactory) { // @formatter:off RestTemplate restTemplate = restTemplateBuilder diff --git a/samples/demo-client/src/main/java/sample/web/AuthorizationController.java b/samples/demo-client/src/main/java/sample/web/AuthorizationController.java index 91480ef4..38e12769 100644 --- a/samples/demo-client/src/main/java/sample/web/AuthorizationController.java +++ b/samples/demo-client/src/main/java/sample/web/AuthorizationController.java @@ -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")) diff --git a/samples/demo-client/src/main/java/sample/web/DeviceController.java b/samples/demo-client/src/main/java/sample/web/DeviceController.java index 9f03d74c..4694c1c4 100644 --- a/samples/demo-client/src/main/java/sample/web/DeviceController.java +++ b/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"); * 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; diff --git a/samples/demo-client/src/main/java/sample/web/JwkSetController.java b/samples/demo-client/src/main/java/sample/web/JwkSetController.java new file mode 100644 index 00000000..46c46122 --- /dev/null +++ b/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 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); + } + +} diff --git a/samples/demo-client/src/main/resources/application.yml b/samples/demo-client/src/main/resources/application.yml index cee14eb7..89f034f7 100644 --- a/samples/demo-client/src/main/resources/application.yml +++ b/samples/demo-client/src/main/resources/application.yml @@ -24,6 +24,18 @@ spring: location: classpath:keystore.p12 password: password 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: cache: false security: @@ -75,6 +87,13 @@ spring: authorization-grant-type: client_credentials scope: message.read,message.write 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: spring: issuer-uri: http://localhost:9000 diff --git a/samples/demo-client/src/main/resources/templates/page-templates.html b/samples/demo-client/src/main/resources/templates/page-templates.html index 71ee8876..a22d3c5f 100644 --- a/samples/demo-client/src/main/resources/templates/page-templates.html +++ b/samples/demo-client/src/main/resources/templates/page-templates.html @@ -26,6 +26,7 @@
  • Authorization Code
  • Client Credentials (client_secret_basic)
  • Client Credentials (tls_client_auth)
  • +
  • Client Credentials (self_signed_tls_client_auth)
  • Token Exchange
  • Device Code