From 5048e60d953af6f7cd19d700fbbf6367d3aa6594 Mon Sep 17 00:00:00 2001 From: prafsoni Date: Tue, 11 Nov 2025 00:03:40 -0800 Subject: [PATCH 1/7] feat: Add initial implementation for oauth2 support in the spring cloud config client Signed-off-by: prafsoni --- spring-cloud-config-client/pom.xml | 4 ++ .../config/client/ConfigClientProperties.java | 62 ++++++++++++++++- .../ConfigClientRequestTemplateFactory.java | 69 ++++++++++++++++++- 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/spring-cloud-config-client/pom.xml b/spring-cloud-config-client/pom.xml index 0543ac844f..73d40366a0 100644 --- a/spring-cloud-config-client/pom.xml +++ b/spring-cloud-config-client/pom.xml @@ -58,6 +58,10 @@ spring-boot-starter-actuator true + + org.springframework.boot + spring-boot-starter-security-oauth2-client + org.springframework.boot spring-boot-starter-aspectj diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java index 28bfb3fd3b..816e60d2d6 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java @@ -28,6 +28,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; import org.springframework.cloud.config.environment.EnvironmentMediaType; import org.springframework.cloud.configuration.TlsProperties; import org.springframework.core.env.Environment; @@ -185,6 +186,8 @@ public class ConfigClientProperties { */ private boolean sendAllLabels = false; + private OAuth2Properties oauth2 = new OAuth2Properties(); + ConfigClientProperties() { } @@ -352,6 +355,14 @@ public void setSendAllLabels(boolean sendAllLabels) { this.sendAllLabels = sendAllLabels; } + public OAuth2Properties getOauth2() { + return oauth2; + } + + public void setOauth2(OAuth2Properties oauth2) { + this.oauth2 = oauth2; + } + private Credentials extractCredentials(int index) { Credentials result = new Credentials(); int noOfUrl = this.uri.length; @@ -441,7 +452,8 @@ public String toString() { + Arrays.toString(this.uri) + ", mediaType=" + this.mediaType + ", discovery=" + this.discovery + ", failFast=" + this.failFast + ", token=" + this.token + ", requestConnectTimeout=" + this.requestConnectTimeout + ", requestReadTimeout=" + this.requestReadTimeout + ", sendState=" - + this.sendState + ", headers=" + this.headers + ", sendAllLabels=" + this.sendAllLabels + "]"; + + this.sendState + ", headers=" + this.headers + ", sendAllLabels=" + this.sendAllLabels + ", oauth2" + + this.oauth2 + "]"; } /** @@ -526,4 +538,52 @@ public enum MultipleUriStrategy { } + public static class OAuth2Properties { + + /** + * Default client registration id. + */ + public static final String CLIENT_REGISTRATION_ID = "config-oauth2-client"; + + /** + * Flag to say that the remote configuration server is configured with OAuth2. + * Default false.; + */ + private boolean enabled = false; + + private OAuth2ClientProperties.Provider provider = new OAuth2ClientProperties.Provider(); + + private OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public OAuth2ClientProperties.Provider getProvider() { + return provider; + } + + public void setProvider(OAuth2ClientProperties.Provider provider) { + this.provider = provider; + } + + public OAuth2ClientProperties.Registration getRegistration() { + return registration; + } + + public void setRegistration(OAuth2ClientProperties.Registration registration) { + this.registration = registration; + } + + @Override + public String toString() { + return "OAuth2Properties [" + "enabled=" + enabled + "]"; + } + + } + } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java index 6ceca390f0..b02437c96f 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java @@ -18,9 +18,10 @@ import java.io.IOException; import java.security.GeneralSecurityException; -import java.util.Arrays; +import java.util.ArrayList; import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -35,6 +36,8 @@ import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.util.Timeout; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientPropertiesMapper; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; @@ -44,6 +47,16 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; +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.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; @@ -77,15 +90,67 @@ public RestTemplate create() { ClientHttpRequestFactory requestFactory = createHttpRequestFactory(properties); RestTemplate template = new RestTemplate(requestFactory); + + final List interceptors = new ArrayList<>(); Map headers = new HashMap<>(properties.getHeaders()); headers.remove(AUTHORIZATION); // To avoid redundant addition of header if (!headers.isEmpty()) { - template.setInterceptors(Arrays.asList(new GenericRequestHeaderInterceptor(headers))); + interceptors.add(new GenericRequestHeaderInterceptor(headers)); + } + + if (properties.getOauth2().isEnabled()) { + ClientHttpRequestInterceptor oauth2Interceptor = createOauth2Interceptor(properties.getOauth2()); + interceptors.add(oauth2Interceptor); } + template.setInterceptors(interceptors); return template; } + private ClientHttpRequestInterceptor createOauth2Interceptor(ConfigClientProperties.OAuth2Properties properties) { + final OAuth2AuthorizedClientManager authorizedClientManager = createAuthorizedClientManager(properties); + OAuth2ClientHttpRequestInterceptor oauth2Interceptor = new OAuth2ClientHttpRequestInterceptor( + authorizedClientManager); + oauth2Interceptor + .setClientRegistrationIdResolver(request -> ConfigClientProperties.OAuth2Properties.CLIENT_REGISTRATION_ID); + return oauth2Interceptor; + } + + private OAuth2AuthorizedClientManager createAuthorizedClientManager( + ConfigClientProperties.OAuth2Properties properties) { + + OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .refreshToken() + .build(); + + ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(properties); + + OAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService( + clientRegistrationRepository); + + AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + return authorizedClientManager; + } + + private ClientRegistrationRepository clientRegistrationRepository( + ConfigClientProperties.OAuth2Properties properties) { + OAuth2ClientProperties oauth2ClientProperties = new OAuth2ClientProperties(); + properties.getRegistration().setProvider(null); // In case it was set in config + // properties + oauth2ClientProperties.getRegistration() + .put(ConfigClientProperties.OAuth2Properties.CLIENT_REGISTRATION_ID, properties.getRegistration()); + oauth2ClientProperties.getProvider() + .put(ConfigClientProperties.OAuth2Properties.CLIENT_REGISTRATION_ID, properties.getProvider()); + oauth2ClientProperties.afterPropertiesSet(); + + List registrations = new ArrayList<>( + new OAuth2ClientPropertiesMapper(oauth2ClientProperties).asClientRegistrations().values()); + return new InMemoryClientRegistrationRepository(registrations); + } + protected ClientHttpRequestFactory createHttpRequestFactory(ConfigClientProperties client) { if (client.getTls().isEnabled()) { try { From 43c14d847b0106f88537e37b81d227bfe05b7a3e Mon Sep 17 00:00:00 2001 From: prafsoni Date: Tue, 11 Nov 2025 11:29:25 -0800 Subject: [PATCH 2/7] test: Add integration tests for config client oauth2 support Signed-off-by: prafsoni --- pom.xml | 2 + .../pom.xml | 103 ++++++++++++ ...ientRequestTemplateFactoryOAuth2Tests.java | 151 ++++++++++++++++++ .../src/test/resources/test-realm.json | 18 +++ 4 files changed, 274 insertions(+) create mode 100644 spring-cloud-config-client-oauth2-tests/pom.xml create mode 100644 spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java create mode 100644 spring-cloud-config-client-oauth2-tests/src/test/resources/test-realm.json diff --git a/pom.xml b/pom.xml index 2a3d757ad7..6a91a9d6ef 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,7 @@ spring-cloud-config-sample spring-cloud-starter-config spring-cloud-config-client-tls-tests + spring-cloud-config-client-oauth2-tests docs @@ -178,6 +179,7 @@ spring-cloud-config-client-tls-tests + spring-cloud-config-client-oauth2-tests spring-cloud-config-sample diff --git a/spring-cloud-config-client-oauth2-tests/pom.xml b/spring-cloud-config-client-oauth2-tests/pom.xml new file mode 100644 index 0000000000..840ab8398e --- /dev/null +++ b/spring-cloud-config-client-oauth2-tests/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + spring-cloud-config-client-oauth2-tests + jar + Spring Cloud Config Client OAuth2 Tests + + + org.springframework.cloud + spring-cloud-config + 5.0.0-SNAPSHOT + .. + + + https://spring.io + Spring Cloud Config Client OAuth2 Integration Tests + + + + org.springframework.cloud + spring-cloud-config-client + ${project.version} + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework + spring-web + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.platform + junit-platform-launcher + test + + + org.springframework.cloud + spring-cloud-test-support + test + + + org.testcontainers + testcontainers-junit-jupiter + test + + + org.springframework.boot + spring-boot-testcontainers + test + + + com.github.dasniko + testcontainers-keycloak + 3.4.0 + test + + + commons-io + commons-io + 2.20.0 + + + + org.mock-server + mockserver-netty + 5.15.0 + test + + + org.mock-server + mockserver-client-java + 5.15.0 + test + + + + + + + + maven-deploy-plugin + + true + + + + + + diff --git a/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java b/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java new file mode 100644 index 0000000000..760eb8c16d --- /dev/null +++ b/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2013-present 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 org.springframework.cloud.config.client; + +import java.net.URI; + +import dasniko.testcontainers.keycloak.KeycloakContainer; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.mockserver.client.MockServerClient; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.HttpRequest; +import org.mockserver.model.MediaType; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +/** + * IntegrationTest for OAuth2 support in ConfigClientRequestTemplateFactory using Keycloak + * Test container as the Authorization Server and MockServer as a protected resource + * server. + */ +@Tag("DockerRequired") +@Testcontainers +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ConfigClientRequestTemplateFactoryOAuth2Tests { + + private static final Log log = LogFactory.getLog(ConfigClientRequestTemplateFactoryOAuth2Tests.class); + + @Container + static KeycloakContainer keycloak = new KeycloakContainer().withRealmImportFile("test-realm.json"); // classpath + // resource + + private ClientAndServer mockServer; + + private MockServerClient mockClient; + + @BeforeAll + void startMockServer() { + mockServer = ClientAndServer.startClientAndServer(0); + mockClient = new MockServerClient("localhost", mockServer.getLocalPort()); + } + + @BeforeEach + void resetExpectations() { + mockClient.clear(request().withPath("/secure")); + mockClient.when(request().withMethod("GET").withPath("/secure")).respond(request -> { + if (request.containsHeader("Authorization")) { + String authHeader = request.getFirstHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return response().withStatusCode(200).withContentType(MediaType.TEXT_PLAIN).withBody("ok"); + } + } + return response().withStatusCode(401); + }); + } + + @AfterAll + void tearDown() { + if (mockClient != null) { + mockClient.close(); + } + if (mockServer != null) { + mockServer.stop(); + } + } + + @Test + void restTemplateAddsBearerTokenFromKeycloakUsingClientCredentials() { + // given OAuth2 client configuration pointing to Keycloak token endpoint + String tokenUri = keycloak.getAuthServerUrl() + "/realms/test-realm/protocol/openid-connect/token"; + + ConfigClientProperties props = new ConfigClientProperties(); + props.getOauth2().setEnabled(true); + + OAuth2ClientProperties.Provider provider = props.getOauth2().getProvider(); + provider.setTokenUri(tokenUri); + + OAuth2ClientProperties.Registration registration = props.getOauth2().getRegistration(); + registration.setClientId("config-client"); + registration.setClientSecret("my-client-secret"); + registration.setAuthorizationGrantType("client_credentials"); + + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, props); + RestTemplate restTemplate = factory.create(); + + // when + String url = "http://localhost:" + mockServer.getLocalPort() + "/secure"; + ResponseEntity response = restTemplate.getForEntity(URI.create(url), String.class); + + // then + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response.getBody()).isEqualTo("ok"); + + HttpRequest[] recorded = mockClient.retrieveRecordedRequests(request().withPath("/secure")); + assertThat(recorded).hasSize(1); + String authHeader = recorded[0].getFirstHeader("Authorization"); + assertThat(authHeader).isNotNull().startsWith("Bearer "); + } + + @Test + void restTemplateDoesNotAddAuthorizationHeaderWhenOauth2Disabled() { + // given ConfigClientProperties with OAuth2 disabled + ConfigClientProperties props = new ConfigClientProperties(); + props.getOauth2().setEnabled(false); // explicitly disabled + + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, props); + RestTemplate restTemplate = factory.create(); + + // when + String url = "http://localhost:" + mockServer.getLocalPort() + "/secure"; + + assertThatThrownBy(() -> restTemplate.getForEntity(URI.create(url), String.class)) + .isInstanceOf(org.springframework.web.client.HttpClientErrorException.Unauthorized.class); + + // then + + HttpRequest[] recorded = mockClient.retrieveRecordedRequests(request().withPath("/secure")); + assertThat(recorded).hasSize(1); // only this test's request + assertThat(recorded[0].containsHeader("Authorization")).isFalse(); + } + +} diff --git a/spring-cloud-config-client-oauth2-tests/src/test/resources/test-realm.json b/spring-cloud-config-client-oauth2-tests/src/test/resources/test-realm.json new file mode 100644 index 0000000000..7aaed2819b --- /dev/null +++ b/spring-cloud-config-client-oauth2-tests/src/test/resources/test-realm.json @@ -0,0 +1,18 @@ +{ + "realm": "test-realm", + "enabled": true, + "clients": [ + { + "clientId": "config-client", + "secret": "my-client-secret", + "name": "Config Client", + "protocol": "openid-connect", + "publicClient": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "redirectUris": ["*"], + "webOrigins": ["*"] + } + ] +} + From dcd60f994a28bbf1c06a04f9241e2c33c33b1a7a Mon Sep 17 00:00:00 2001 From: prafsoni Date: Tue, 11 Nov 2025 11:44:40 -0800 Subject: [PATCH 3/7] test: Add integration tests for config client oauth2 support using issuer uri Signed-off-by: prafsoni --- ...ientRequestTemplateFactoryOAuth2Tests.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java b/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java index 760eb8c16d..221f95f156 100644 --- a/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java +++ b/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java @@ -126,6 +126,39 @@ void restTemplateAddsBearerTokenFromKeycloakUsingClientCredentials() { assertThat(authHeader).isNotNull().startsWith("Bearer "); } + @Test + void restTemplateAddsBearerTokenFromKeycloakUsingClientCredentialsAndIssuerUri() { + // given OAuth2 client configuration pointing to Keycloak issuer endpoint + String issuerUri = keycloak.getAuthServerUrl() + "/realms/test-realm"; + + ConfigClientProperties props = new ConfigClientProperties(); + props.getOauth2().setEnabled(true); + + OAuth2ClientProperties.Provider provider = props.getOauth2().getProvider(); + provider.setIssuerUri(issuerUri); + + OAuth2ClientProperties.Registration registration = props.getOauth2().getRegistration(); + registration.setClientId("config-client"); + registration.setClientSecret("my-client-secret"); + registration.setAuthorizationGrantType("client_credentials"); + + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, props); + RestTemplate restTemplate = factory.create(); + + // when + String url = "http://localhost:" + mockServer.getLocalPort() + "/secure"; + ResponseEntity response = restTemplate.getForEntity(URI.create(url), String.class); + + // then + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + assertThat(response.getBody()).isEqualTo("ok"); + + HttpRequest[] recorded = mockClient.retrieveRecordedRequests(request().withPath("/secure")); + assertThat(recorded).hasSize(1); + String authHeader = recorded[0].getFirstHeader("Authorization"); + assertThat(authHeader).isNotNull().startsWith("Bearer "); + } + @Test void restTemplateDoesNotAddAuthorizationHeaderWhenOauth2Disabled() { // given ConfigClientProperties with OAuth2 disabled From 9e80608f8c31d10eae569b2e7fdd0fe3bdd96d54 Mon Sep 17 00:00:00 2001 From: prafsoni Date: Tue, 11 Nov 2025 11:46:35 -0800 Subject: [PATCH 4/7] docs: Update client docs to add an example for oauth2 client config Signed-off-by: prafsoni --- docs/modules/ROOT/pages/client.adoc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/modules/ROOT/pages/client.adoc b/docs/modules/ROOT/pages/client.adoc index 1358d3c6a1..e173c46221 100644 --- a/docs/modules/ROOT/pages/client.adoc +++ b/docs/modules/ROOT/pages/client.adoc @@ -214,6 +214,27 @@ spring: The `spring.cloud.config.password` and `spring.cloud.config.username` values override anything that is provided in the URI. +If you use OAuth2 security on the server, clients need to know the client ID and client secret. +You can specify the client ID and client secret via separate properties, as shown in the following example: + +[source,yaml] +---- + +spring: + cloud: + config: + uri: https://myconfig.mycompany.com + oauth2: + enabled: true + provider: + token-uri: https://auth.acme.com/oauth/token + registration: + client-id: client-id + client-secret: client-secret + authorization-grant-type: client_credentials + +---- + If you deploy your apps on Cloud Foundry, the best way to provide the password is through service credentials (such as in the URI, since it does not need to be in a config file). The following example works locally and for a user-provided service on Cloud Foundry named `configserver`: From aedd83ce0128a6d77b7196a47803934ac134c3b2 Mon Sep 17 00:00:00 2001 From: prafsoni Date: Thu, 27 Nov 2025 11:07:10 -0800 Subject: [PATCH 5/7] test: fix pom version after rebase Signed-off-by: prafsoni --- spring-cloud-config-client-oauth2-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-config-client-oauth2-tests/pom.xml b/spring-cloud-config-client-oauth2-tests/pom.xml index 840ab8398e..bcf270e921 100644 --- a/spring-cloud-config-client-oauth2-tests/pom.xml +++ b/spring-cloud-config-client-oauth2-tests/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-config - 5.0.0-SNAPSHOT + 5.0.1-SNAPSHOT .. From 244c06ae8132196e2326bd3684ee7bdf9c7cf489 Mon Sep 17 00:00:00 2001 From: prafsoni Date: Wed, 4 Mar 2026 00:01:02 -0800 Subject: [PATCH 6/7] chore: fix pom version after rebase Signed-off-by: prafsoni --- spring-cloud-config-client-oauth2-tests/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-cloud-config-client-oauth2-tests/pom.xml b/spring-cloud-config-client-oauth2-tests/pom.xml index bcf270e921..6451b54c0f 100644 --- a/spring-cloud-config-client-oauth2-tests/pom.xml +++ b/spring-cloud-config-client-oauth2-tests/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-config - 5.0.1-SNAPSHOT + 5.0.2-SNAPSHOT .. From f2ccbd250fe0b7d75f6e871a0c1cab9117558f7d Mon Sep 17 00:00:00 2001 From: prafsoni Date: Sat, 6 Jun 2026 17:42:00 -0700 Subject: [PATCH 7/7] refactor: Make config-client OAuth2 support pluggable and optional Move OAuth2 wiring into a new oauth2 sub-package gated by ClassUtils.isPresent so spring-boot-starter-security-oauth2-client is genuinely optional. Replace the duplicate spring.cloud.config.oauth2 registration/provider schema with a client-registration-id that points at a standard Spring Security registration under spring.security.oauth2.client.*. ConfigClientRequestTemplateFactory no longer references Spring Security; it just accepts additional interceptors. The interceptor is wired for both the Config Data flow (ConfigClientOAuth2Support.registerInterceptor via the bootstrap context, with the bind handler threaded so encrypted/placeholder secrets resolve) and the legacy bootstrap flow (ConfigClientOAuth2BootstrapConfiguration beans). Every collaborator (ClientRegistrationRepository, OAuth2AuthorizedClientManager, ClientRegistrationIdResolver, and the interceptor itself) is overridable, and the default repository is built lazily so a user-supplied manager makes it unnecessary. Signed-off-by: prafsoni Co-Authored-By: Claude Opus 4.7 --- docs/modules/ROOT/pages/client.adoc | 44 ++-- .../pom.xml | 6 +- ...ientRequestTemplateFactoryOAuth2Tests.java | 122 +++++++++--- spring-cloud-config-client/pom.xml | 1 + .../config/client/ConfigClientProperties.java | 62 +----- .../ConfigClientRequestTemplateFactory.java | 84 +++----- ...onfigServerConfigDataLocationResolver.java | 10 +- .../ConfigServiceBootstrapConfiguration.java | 13 +- .../ConfigServicePropertySourceLocator.java | 18 +- ...figClientOAuth2BootstrapConfiguration.java | 91 +++++++++ .../oauth2/ConfigClientOAuth2Properties.java | 63 ++++++ .../oauth2/ConfigClientOAuth2Support.java | 188 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 1 + 13 files changed, 531 insertions(+), 172 deletions(-) create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2BootstrapConfiguration.java create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Properties.java create mode 100644 spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Support.java diff --git a/docs/modules/ROOT/pages/client.adoc b/docs/modules/ROOT/pages/client.adoc index e173c46221..fc37c9bbb2 100644 --- a/docs/modules/ROOT/pages/client.adoc +++ b/docs/modules/ROOT/pages/client.adoc @@ -214,27 +214,45 @@ spring: The `spring.cloud.config.password` and `spring.cloud.config.username` values override anything that is provided in the URI. -If you use OAuth2 security on the server, clients need to know the client ID and client secret. -You can specify the client ID and client secret via separate properties, as shown in the following example: +If your Config Server is secured with OAuth2, the Config Client can attach a bearer token +to every request. Configure the OAuth2 client registration using the standard Spring +Security properties and point the Config Client at it by id: [source,yaml] ---- - spring: cloud: config: - uri: https://myconfig.mycompany.com - oauth2: - enabled: true - provider: - token-uri: https://auth.acme.com/oauth/token - registration: - client-id: client-id - client-secret: client-secret - authorization-grant-type: client_credentials - + uri: https://myconfig.mycompany.com + oauth2: + enabled: true + client-registration-id: config-client + security: + oauth2: + client: + registration: + config-client: + client-id: client-id + client-secret: client-secret + authorization-grant-type: client_credentials + provider: + config-client: + token-uri: https://auth.acme.com/oauth/token ---- +`spring-boot-starter-security-oauth2-client` is an optional dependency. The Config Client +attaches the OAuth2 bearer-token interceptor only when that starter is on the classpath +and `spring.cloud.config.oauth2.enabled=true`; otherwise the interceptor is skipped. This +works for both the Config Data import flow (`spring.config.import=configserver:`) and the +legacy bootstrap flow. By default the Config Client builds an +`InMemoryClientRegistrationRepository` from the properties above and an +`OAuth2AuthorizedClientManager` that supports the `client_credentials` and `refresh_token` +grant types. Each layer is overridable: register your own `ClientRegistrationRepository`, +`OAuth2AuthorizedClientManager`, `ClientRegistrationIdResolver`, or the +`ClientHttpRequestInterceptor` itself (in the bootstrap registry via a +`BootstrapRegistryInitializer` for the Config Data flow, or as a bean for the legacy +bootstrap flow) and the default will step aside. + If you deploy your apps on Cloud Foundry, the best way to provide the password is through service credentials (such as in the URI, since it does not need to be in a config file). The following example works locally and for a user-provided service on Cloud Foundry named `configserver`: diff --git a/spring-cloud-config-client-oauth2-tests/pom.xml b/spring-cloud-config-client-oauth2-tests/pom.xml index 6451b54c0f..dc41b8042e 100644 --- a/spring-cloud-config-client-oauth2-tests/pom.xml +++ b/spring-cloud-config-client-oauth2-tests/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-config - 5.0.2-SNAPSHOT + 5.0.4-SNAPSHOT .. @@ -31,6 +31,10 @@ org.springframework.boot spring-boot-autoconfigure + + org.springframework.boot + spring-boot-starter-security-oauth2-client + org.springframework spring-web diff --git a/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java b/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java index 221f95f156..45c079c751 100644 --- a/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java +++ b/spring-cloud-config-client-oauth2-tests/src/test/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactoryOAuth2Tests.java @@ -17,6 +17,9 @@ package org.springframework.cloud.config.client; import java.net.URI; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import dasniko.testcontainers.keycloak.KeycloakContainer; import org.apache.commons.logging.Log; @@ -34,8 +37,16 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; -import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; +import org.springframework.boot.bootstrap.DefaultBootstrapContext; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.cloud.config.client.oauth2.ConfigClientOAuth2Support; import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; @@ -47,6 +58,12 @@ * IntegrationTest for OAuth2 support in ConfigClientRequestTemplateFactory using Keycloak * Test container as the Authorization Server and MockServer as a protected resource * server. + * + *

+ * Each test drives the real wiring entry point + * {@link ConfigClientOAuth2Support#registerInterceptor} with bound properties, exactly as + * the Config Data flow does at runtime. + *

*/ @Tag("DockerRequired") @Testcontainers @@ -55,6 +72,8 @@ class ConfigClientRequestTemplateFactoryOAuth2Tests { private static final Log log = LogFactory.getLog(ConfigClientRequestTemplateFactoryOAuth2Tests.class); + private static final String REGISTRATION_ID = "config-client"; + @Container static KeycloakContainer keycloak = new KeycloakContainer().withRealmImportFile("test-realm.json"); // classpath // resource @@ -98,19 +117,10 @@ void restTemplateAddsBearerTokenFromKeycloakUsingClientCredentials() { // given OAuth2 client configuration pointing to Keycloak token endpoint String tokenUri = keycloak.getAuthServerUrl() + "/realms/test-realm/protocol/openid-connect/token"; - ConfigClientProperties props = new ConfigClientProperties(); - props.getOauth2().setEnabled(true); - - OAuth2ClientProperties.Provider provider = props.getOauth2().getProvider(); - provider.setTokenUri(tokenUri); - - OAuth2ClientProperties.Registration registration = props.getOauth2().getRegistration(); - registration.setClientId("config-client"); - registration.setClientSecret("my-client-secret"); - registration.setAuthorizationGrantType("client_credentials"); + Map properties = baseOAuth2Properties(); + properties.put("spring.security.oauth2.client.provider." + REGISTRATION_ID + ".token-uri", tokenUri); - ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, props); - RestTemplate restTemplate = factory.create(); + RestTemplate restTemplate = createRestTemplate(new DefaultBootstrapContext(), properties); // when String url = "http://localhost:" + mockServer.getLocalPort() + "/secure"; @@ -131,19 +141,10 @@ void restTemplateAddsBearerTokenFromKeycloakUsingClientCredentialsAndIssuerUri() // given OAuth2 client configuration pointing to Keycloak issuer endpoint String issuerUri = keycloak.getAuthServerUrl() + "/realms/test-realm"; - ConfigClientProperties props = new ConfigClientProperties(); - props.getOauth2().setEnabled(true); + Map properties = baseOAuth2Properties(); + properties.put("spring.security.oauth2.client.provider." + REGISTRATION_ID + ".issuer-uri", issuerUri); - OAuth2ClientProperties.Provider provider = props.getOauth2().getProvider(); - provider.setIssuerUri(issuerUri); - - OAuth2ClientProperties.Registration registration = props.getOauth2().getRegistration(); - registration.setClientId("config-client"); - registration.setClientSecret("my-client-secret"); - registration.setAuthorizationGrantType("client_credentials"); - - ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, props); - RestTemplate restTemplate = factory.create(); + RestTemplate restTemplate = createRestTemplate(new DefaultBootstrapContext(), properties); // when String url = "http://localhost:" + mockServer.getLocalPort() + "/secure"; @@ -161,12 +162,11 @@ void restTemplateAddsBearerTokenFromKeycloakUsingClientCredentialsAndIssuerUri() @Test void restTemplateDoesNotAddAuthorizationHeaderWhenOauth2Disabled() { - // given ConfigClientProperties with OAuth2 disabled - ConfigClientProperties props = new ConfigClientProperties(); - props.getOauth2().setEnabled(false); // explicitly disabled + // given config-client OAuth2 disabled + Map properties = new HashMap<>(); + properties.put("spring.cloud.config.oauth2.enabled", "false"); - ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, props); - RestTemplate restTemplate = factory.create(); + RestTemplate restTemplate = createRestTemplate(new DefaultBootstrapContext(), properties); // when String url = "http://localhost:" + mockServer.getLocalPort() + "/secure"; @@ -175,10 +175,70 @@ void restTemplateDoesNotAddAuthorizationHeaderWhenOauth2Disabled() { .isInstanceOf(org.springframework.web.client.HttpClientErrorException.Unauthorized.class); // then - HttpRequest[] recorded = mockClient.retrieveRecordedRequests(request().withPath("/secure")); assertThat(recorded).hasSize(1); // only this test's request assertThat(recorded[0].containsHeader("Authorization")).isFalse(); } + @Test + void userSuppliedAuthorizedClientManagerWins() { + // given a bootstrap context with a user-supplied OAuth2AuthorizedClientManager + // already registered — Keycloak is not configured at all, proving the default + // path is bypassed. + Map properties = new HashMap<>(); + properties.put("spring.cloud.config.oauth2.enabled", "true"); + properties.put("spring.cloud.config.oauth2.client-registration-id", REGISTRATION_ID); + + DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext(); + OAuth2AuthorizedClientManager stubManager = stubManagerReturning("user-supplied-token"); + bootstrapContext.register(OAuth2AuthorizedClientManager.class, ctx -> stubManager); + + RestTemplate restTemplate = createRestTemplate(bootstrapContext, properties); + + // when + String url = "http://localhost:" + mockServer.getLocalPort() + "/secure"; + ResponseEntity response = restTemplate.getForEntity(URI.create(url), String.class); + + // then + assertThat(response.getStatusCode().is2xxSuccessful()).isTrue(); + HttpRequest[] recorded = mockClient.retrieveRecordedRequests(request().withPath("/secure")); + assertThat(recorded).hasSize(1); + assertThat(recorded[0].getFirstHeader("Authorization")).isEqualTo("Bearer user-supplied-token"); + } + + private static Map baseOAuth2Properties() { + Map properties = new HashMap<>(); + properties.put("spring.cloud.config.oauth2.enabled", "true"); + properties.put("spring.cloud.config.oauth2.client-registration-id", REGISTRATION_ID); + properties.put("spring.security.oauth2.client.registration." + REGISTRATION_ID + ".client-id", "config-client"); + properties.put("spring.security.oauth2.client.registration." + REGISTRATION_ID + ".client-secret", + "my-client-secret"); + properties.put("spring.security.oauth2.client.registration." + REGISTRATION_ID + ".authorization-grant-type", + "client_credentials"); + return properties; + } + + private static RestTemplate createRestTemplate(DefaultBootstrapContext bootstrapContext, + Map properties) { + Binder binder = new Binder(new MapConfigurationPropertySource(properties)); + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, + new ConfigClientProperties()); + ConfigClientOAuth2Support.registerInterceptor(bootstrapContext, binder, null, factory); + return factory.create(); + } + + private static OAuth2AuthorizedClientManager stubManagerReturning(String tokenValue) { + ClientRegistration registration = ClientRegistration.withRegistrationId(REGISTRATION_ID) + .clientId("config-client") + .clientSecret("my-client-secret") + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .tokenUri("http://stub/token") + .build(); + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, tokenValue, + Instant.now(), Instant.now().plusSeconds(60)); + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(registration, "config-client", + accessToken); + return authorizeRequest -> authorizedClient; + } + } diff --git a/spring-cloud-config-client/pom.xml b/spring-cloud-config-client/pom.xml index c78d7d573e..458fe7538e 100644 --- a/spring-cloud-config-client/pom.xml +++ b/spring-cloud-config-client/pom.xml @@ -61,6 +61,7 @@ org.springframework.boot spring-boot-starter-security-oauth2-client + true org.springframework.boot diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java index 816e60d2d6..28bfb3fd3b 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientProperties.java @@ -28,7 +28,6 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; import org.springframework.cloud.config.environment.EnvironmentMediaType; import org.springframework.cloud.configuration.TlsProperties; import org.springframework.core.env.Environment; @@ -186,8 +185,6 @@ public class ConfigClientProperties { */ private boolean sendAllLabels = false; - private OAuth2Properties oauth2 = new OAuth2Properties(); - ConfigClientProperties() { } @@ -355,14 +352,6 @@ public void setSendAllLabels(boolean sendAllLabels) { this.sendAllLabels = sendAllLabels; } - public OAuth2Properties getOauth2() { - return oauth2; - } - - public void setOauth2(OAuth2Properties oauth2) { - this.oauth2 = oauth2; - } - private Credentials extractCredentials(int index) { Credentials result = new Credentials(); int noOfUrl = this.uri.length; @@ -452,8 +441,7 @@ public String toString() { + Arrays.toString(this.uri) + ", mediaType=" + this.mediaType + ", discovery=" + this.discovery + ", failFast=" + this.failFast + ", token=" + this.token + ", requestConnectTimeout=" + this.requestConnectTimeout + ", requestReadTimeout=" + this.requestReadTimeout + ", sendState=" - + this.sendState + ", headers=" + this.headers + ", sendAllLabels=" + this.sendAllLabels + ", oauth2" - + this.oauth2 + "]"; + + this.sendState + ", headers=" + this.headers + ", sendAllLabels=" + this.sendAllLabels + "]"; } /** @@ -538,52 +526,4 @@ public enum MultipleUriStrategy { } - public static class OAuth2Properties { - - /** - * Default client registration id. - */ - public static final String CLIENT_REGISTRATION_ID = "config-oauth2-client"; - - /** - * Flag to say that the remote configuration server is configured with OAuth2. - * Default false.; - */ - private boolean enabled = false; - - private OAuth2ClientProperties.Provider provider = new OAuth2ClientProperties.Provider(); - - private OAuth2ClientProperties.Registration registration = new OAuth2ClientProperties.Registration(); - - public boolean isEnabled() { - return enabled; - } - - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - public OAuth2ClientProperties.Provider getProvider() { - return provider; - } - - public void setProvider(OAuth2ClientProperties.Provider provider) { - this.provider = provider; - } - - public OAuth2ClientProperties.Registration getRegistration() { - return registration; - } - - public void setRegistration(OAuth2ClientProperties.Registration registration) { - this.registration = registration; - } - - @Override - public String toString() { - return "OAuth2Properties [" + "enabled=" + enabled + "]"; - } - - } - } diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java index b02437c96f..dfb1ba332c 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigClientRequestTemplateFactory.java @@ -20,6 +20,7 @@ import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -36,8 +37,6 @@ import org.apache.hc.core5.http.io.SocketConfig; import org.apache.hc.core5.util.Timeout; -import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; -import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientPropertiesMapper; import org.springframework.cloud.configuration.SSLContextFactory; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; @@ -47,16 +46,6 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; -import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; -import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; -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.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; import static org.springframework.cloud.config.client.ConfigClientProperties.AUTHORIZATION; @@ -67,9 +56,30 @@ public class ConfigClientRequestTemplateFactory { private final ConfigClientProperties properties; + private final List additionalInterceptors = new ArrayList<>(); + public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties) { + this(log, properties, Collections.emptyList()); + } + + public ConfigClientRequestTemplateFactory(Log log, ConfigClientProperties properties, + List additionalInterceptors) { this.log = log; this.properties = properties; + if (additionalInterceptors != null) { + this.additionalInterceptors.addAll(additionalInterceptors); + } + } + + /** + * Add an interceptor to be applied to the {@link RestTemplate} returned by + * {@link #create()}. Used to attach authentication interceptors (for example the + * OAuth2 bearer-token interceptor) without coupling this factory to a specific + * security implementation. + * @param interceptor the interceptor to add + */ + public void addInterceptor(ClientHttpRequestInterceptor interceptor) { + this.additionalInterceptors.add(interceptor); } public Log getLog() { @@ -97,60 +107,12 @@ public RestTemplate create() { if (!headers.isEmpty()) { interceptors.add(new GenericRequestHeaderInterceptor(headers)); } - - if (properties.getOauth2().isEnabled()) { - ClientHttpRequestInterceptor oauth2Interceptor = createOauth2Interceptor(properties.getOauth2()); - interceptors.add(oauth2Interceptor); - } + interceptors.addAll(this.additionalInterceptors); template.setInterceptors(interceptors); return template; } - private ClientHttpRequestInterceptor createOauth2Interceptor(ConfigClientProperties.OAuth2Properties properties) { - final OAuth2AuthorizedClientManager authorizedClientManager = createAuthorizedClientManager(properties); - OAuth2ClientHttpRequestInterceptor oauth2Interceptor = new OAuth2ClientHttpRequestInterceptor( - authorizedClientManager); - oauth2Interceptor - .setClientRegistrationIdResolver(request -> ConfigClientProperties.OAuth2Properties.CLIENT_REGISTRATION_ID); - return oauth2Interceptor; - } - - private OAuth2AuthorizedClientManager createAuthorizedClientManager( - ConfigClientProperties.OAuth2Properties properties) { - - OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() - .clientCredentials() - .refreshToken() - .build(); - - ClientRegistrationRepository clientRegistrationRepository = clientRegistrationRepository(properties); - - OAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService( - clientRegistrationRepository); - - AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( - clientRegistrationRepository, authorizedClientService); - authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); - return authorizedClientManager; - } - - private ClientRegistrationRepository clientRegistrationRepository( - ConfigClientProperties.OAuth2Properties properties) { - OAuth2ClientProperties oauth2ClientProperties = new OAuth2ClientProperties(); - properties.getRegistration().setProvider(null); // In case it was set in config - // properties - oauth2ClientProperties.getRegistration() - .put(ConfigClientProperties.OAuth2Properties.CLIENT_REGISTRATION_ID, properties.getRegistration()); - oauth2ClientProperties.getProvider() - .put(ConfigClientProperties.OAuth2Properties.CLIENT_REGISTRATION_ID, properties.getProvider()); - oauth2ClientProperties.afterPropertiesSet(); - - List registrations = new ArrayList<>( - new OAuth2ClientPropertiesMapper(oauth2ClientProperties).asClientRegistrations().values()); - return new InMemoryClientRegistrationRepository(registrations); - } - protected ClientHttpRequestFactory createHttpRequestFactory(ConfigClientProperties client) { if (client.getTls().isEnabled()) { try { diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java index 5e94ab0a65..78dcb07b22 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServerConfigDataLocationResolver.java @@ -41,6 +41,7 @@ import org.springframework.cloud.bootstrap.encrypt.RsaProperties; import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils; import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.config.client.oauth2.ConfigClientOAuth2Support; import org.springframework.cloud.context.encrypt.EncryptorFactory; import org.springframework.core.Ordered; import org.springframework.core.log.LogMessage; @@ -240,8 +241,13 @@ public List resolveProfileSpecific( .registerSingleton("configDataConfigClientProperties", event.getBootstrapContext().get(ConfigClientProperties.class))); - bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, - context -> new ConfigClientRequestTemplateFactory(log, context.get(ConfigClientProperties.class))); + bootstrapContext.registerIfAbsent(ConfigClientRequestTemplateFactory.class, context -> { + ConfigClientRequestTemplateFactory factory = new ConfigClientRequestTemplateFactory(log, + context.get(ConfigClientProperties.class)); + ConfigClientOAuth2Support.registerInterceptor(bootstrapContext, resolverContext.getBinder(), + getBindHandler(resolverContext), factory); + return factory; + }); bootstrapContext.registerIfAbsent(RestTemplate.class, context -> { ConfigClientRequestTemplateFactory factory = context.get(ConfigClientRequestTemplateFactory.class); diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java index e3dfeb10de..ecc12090e9 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServiceBootstrapConfiguration.java @@ -16,18 +16,24 @@ package org.springframework.cloud.config.client; +import java.util.List; + import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.config.client.oauth2.ConfigClientOAuth2BootstrapConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.retry.annotation.EnableRetry; import org.springframework.retry.annotation.Retryable; import org.springframework.retry.backoff.ExponentialBackOffPolicy; @@ -57,8 +63,11 @@ public ConfigClientProperties configClientProperties() { @Bean @ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class) @ConditionalOnProperty(name = ConfigClientProperties.PREFIX + ".enabled", matchIfMissing = true) - public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties) { - return new ConfigServicePropertySourceLocator(properties); + public ConfigServicePropertySourceLocator configServicePropertySource(ConfigClientProperties properties, + @Qualifier(ConfigClientOAuth2BootstrapConfiguration.OAUTH2_INTERCEPTOR_BEAN_NAME) ObjectProvider oauth2Interceptor) { + ConfigServicePropertySourceLocator locator = new ConfigServicePropertySourceLocator(properties); + oauth2Interceptor.ifAvailable(interceptor -> locator.setAdditionalInterceptors(List.of(interceptor))); + return locator; } @ConditionalOnProperty(ConfigClientProperties.PREFIX + ".fail-fast") diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java index b6a00bc62b..92bc1a7122 100644 --- a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/ConfigServicePropertySourceLocator.java @@ -50,6 +50,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.retry.annotation.Retryable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -76,10 +77,25 @@ public class ConfigServicePropertySourceLocator implements PropertySourceLocator private ConfigClientProperties defaultProperties; + private List additionalInterceptors = Collections.emptyList(); + public ConfigServicePropertySourceLocator(ConfigClientProperties defaultProperties) { this.defaultProperties = defaultProperties; } + /** + * Additional {@link ClientHttpRequestInterceptor}s applied to the + * {@link RestTemplate} built per + * {@link #locate(org.springframework.core.env.Environment)} call. Used by + * {@link ConfigServiceBootstrapConfiguration} to attach the OAuth2 bearer-token + * interceptor when {@code spring-security-oauth2-client} is on the classpath. + * @param additionalInterceptors the interceptors to apply + */ + public void setAdditionalInterceptors(List additionalInterceptors) { + this.additionalInterceptors = (additionalInterceptors != null) ? additionalInterceptors + : Collections.emptyList(); + } + /** * Combine the active and default profiles from the environment. * @param properties config client properties, @@ -124,7 +140,7 @@ public org.springframework.core.env.PropertySource locate(org.springframework CompositePropertySource composite = new OriginTrackedCompositePropertySource("configService"); ConfigClientRequestTemplateFactory requestTemplateFactory = new ConfigClientRequestTemplateFactory(logger, - properties); + properties, this.additionalInterceptors); Exception error = null; String errorBody = null; diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2BootstrapConfiguration.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2BootstrapConfiguration.java new file mode 100644 index 0000000000..67911c0445 --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2BootstrapConfiguration.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-present 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 org.springframework.cloud.config.client.oauth2; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; +import org.springframework.cloud.config.client.oauth2.ConfigClientOAuth2Support.OAuth2InterceptorRegistrar; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor; +import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver; + +/** + * Bootstrap configuration for OAuth2 support in the legacy bootstrap flow. + * + *

+ * Spring Security's own auto-configuration does not run inside the Spring Cloud bootstrap + * context, so {@link OAuth2ClientProperties} binding is enabled here explicitly in order + * to build the default {@link ClientRegistrationRepository} from + * {@code spring.security.oauth2.client.*}. + *

+ * + *

+ * Each layer of the chain is a {@link ConditionalOnMissingBean} so a user may override + * any piece (the {@link ClientRegistrationRepository}, the + * {@link OAuth2AuthorizedClientManager}, the {@link ClientRegistrationIdResolver}, or the + * {@link #configClientOAuth2Interceptor} bean itself) without rewriting the rest. + *

+ */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ OAuth2ClientHttpRequestInterceptor.class, ClientRegistrationRepository.class }) +@ConditionalOnProperty(name = ConfigClientOAuth2Properties.PREFIX + ".enabled") +@EnableConfigurationProperties({ ConfigClientOAuth2Properties.class, OAuth2ClientProperties.class }) +public class ConfigClientOAuth2BootstrapConfiguration { + + /** + * Bean name of the OAuth2 {@link ClientHttpRequestInterceptor} consumed by the legacy + * bootstrap flow. + */ + public static final String OAUTH2_INTERCEPTOR_BEAN_NAME = "configClientOAuth2Interceptor"; + + @Bean + @ConditionalOnMissingBean + public ClientRegistrationRepository configClientClientRegistrationRepository(OAuth2ClientProperties properties) { + return OAuth2InterceptorRegistrar.buildClientRegistrationRepository(properties); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizedClientManager configClientAuthorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository) { + return OAuth2InterceptorRegistrar.buildAuthorizedClientManager(clientRegistrationRepository); + } + + @Bean + @ConditionalOnMissingBean + public ClientRegistrationIdResolver configClientRegistrationIdResolver(ConfigClientOAuth2Properties properties) { + String clientRegistrationId = OAuth2InterceptorRegistrar + .requireClientRegistrationId(properties.getClientRegistrationId()); + return request -> clientRegistrationId; + } + + @Bean(name = OAUTH2_INTERCEPTOR_BEAN_NAME) + @ConditionalOnMissingBean(name = OAUTH2_INTERCEPTOR_BEAN_NAME) + public ClientHttpRequestInterceptor configClientOAuth2Interceptor( + OAuth2AuthorizedClientManager authorizedClientManager, + ClientRegistrationIdResolver clientRegistrationIdResolver) { + return OAuth2InterceptorRegistrar.buildInterceptor(authorizedClientManager, clientRegistrationIdResolver); + } + +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Properties.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Properties.java new file mode 100644 index 0000000000..2f85076693 --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Properties.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-present 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 org.springframework.cloud.config.client.oauth2; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configures OAuth2 token acquisition for outbound calls from the config client to the + * config server. Token acquisition itself is configured under the standard + * {@code spring.security.oauth2.client.*} properties; this class only opts the config + * client in and selects which registration to use. + */ +@ConfigurationProperties(ConfigClientOAuth2Properties.PREFIX) +public class ConfigClientOAuth2Properties { + + /** + * Configuration prefix for config-client OAuth2 properties. + */ + public static final String PREFIX = "spring.cloud.config.oauth2"; + + /** + * Whether to attach an OAuth2 bearer token to outbound config-client requests. + */ + private boolean enabled = false; + + /** + * Spring Security client registration id (as configured under + * {@code spring.security.oauth2.client.registration.}) to use when fetching the + * access token. + */ + private String clientRegistrationId; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getClientRegistrationId() { + return clientRegistrationId; + } + + public void setClientRegistrationId(String clientRegistrationId) { + this.clientRegistrationId = clientRegistrationId; + } + +} diff --git a/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Support.java b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Support.java new file mode 100644 index 0000000000..5a23a77197 --- /dev/null +++ b/spring-cloud-config-client/src/main/java/org/springframework/cloud/config/client/oauth2/ConfigClientOAuth2Support.java @@ -0,0 +1,188 @@ +/* + * Copyright 2013-present 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 org.springframework.cloud.config.client.oauth2; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.bootstrap.ConfigurableBootstrapContext; +import org.springframework.boot.context.properties.bind.BindHandler; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientProperties; +import org.springframework.boot.security.oauth2.client.autoconfigure.OAuth2ClientPropertiesMapper; +import org.springframework.cloud.config.client.ConfigClientRequestTemplateFactory; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; +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.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor; +import org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor.ClientRegistrationIdResolver; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Wires the OAuth2 {@link ClientHttpRequestInterceptor} into a + * {@link ConfigClientRequestTemplateFactory} for the Spring Cloud Config Data flow, when + * Spring Security OAuth2 client classes are on the classpath and + * {@code spring.cloud.config.oauth2.enabled=true}. + * + *

+ * Every collaborator that gets built ({@link ClientRegistrationRepository}, + * {@link OAuth2AuthorizedClientManager}, {@link ClientRegistrationIdResolver}, and the + * interceptor itself) is looked up in the bootstrap context first via + * {@link ConfigurableBootstrapContext#getOrElseSupply}, so users can plug in custom + * implementations by registering them in the bootstrap registry before this support class + * runs. + *

+ * + *

+ * All Spring Security references are isolated in the nested + * {@link OAuth2InterceptorRegistrar}, which is only loaded after the + * {@link #OAUTH2_CLIENT_IS_PRESENT} guard passes, so config clients without Spring + * Security OAuth2 on the classpath never trigger class loading of those types. + *

+ */ +public final class ConfigClientOAuth2Support { + + /** + * Fully-qualified class name of the OAuth2 request interceptor used as the classpath + * probe. + */ + public static final String OAUTH2_INTERCEPTOR_CLASS = "org.springframework.security.oauth2.client.web.client.OAuth2ClientHttpRequestInterceptor"; + + /** + * Whether Spring Security OAuth2 client classes are on the classpath. + */ + public static final boolean OAUTH2_CLIENT_IS_PRESENT = ClassUtils.isPresent(OAUTH2_INTERCEPTOR_CLASS, null); + + private ConfigClientOAuth2Support() { + } + + /** + * Adds an OAuth2 interceptor to the given factory if Spring Security OAuth2 client is + * available and {@code spring.cloud.config.oauth2.enabled=true}. Otherwise this is a + * no-op. + * @param bootstrapContext the bootstrap context used to look up overridable + * collaborators + * @param binder the binder used to read OAuth2 properties + * @param bindHandler the bind handler (may be {@code null}) so encrypted or + * placeholder values resolve consistently with the rest of the config client + * @param factory the factory to add the interceptor to + */ + public static void registerInterceptor(ConfigurableBootstrapContext bootstrapContext, Binder binder, + BindHandler bindHandler, ConfigClientRequestTemplateFactory factory) { + if (!OAUTH2_CLIENT_IS_PRESENT) { + return; + } + OAuth2InterceptorRegistrar.register(bootstrapContext, binder, bindHandler, factory); + } + + /** + * Holds every Spring Security reference. Loaded only after the + * {@link #OAUTH2_CLIENT_IS_PRESENT} guard in {@link #registerInterceptor}, or from + * the {@code @ConditionalOnClass}-guarded bootstrap configuration, so config clients + * without Spring Security OAuth2 on the classpath never trigger class loading here. + */ + static final class OAuth2InterceptorRegistrar { + + private OAuth2InterceptorRegistrar() { + } + + static void register(ConfigurableBootstrapContext bootstrapContext, Binder binder, BindHandler bindHandler, + ConfigClientRequestTemplateFactory factory) { + ConfigClientOAuth2Properties oauth2Properties = binder + .bind(ConfigClientOAuth2Properties.PREFIX, Bindable.of(ConfigClientOAuth2Properties.class), bindHandler) + .orElseGet(ConfigClientOAuth2Properties::new); + if (!oauth2Properties.isEnabled()) { + return; + } + String clientRegistrationId = requireClientRegistrationId(oauth2Properties.getClientRegistrationId()); + + // Resolve the manager first. The default ClientRegistrationRepository is + // built + // lazily inside the manager supplier, so a user-supplied manager makes the + // default repository unnecessary. + OAuth2AuthorizedClientManager authorizedClientManager = bootstrapContext.getOrElseSupply( + OAuth2AuthorizedClientManager.class, + () -> buildAuthorizedClientManager( + bootstrapContext.getOrElseSupply(ClientRegistrationRepository.class, + () -> buildClientRegistrationRepository(binder, bindHandler)))); + + ClientRegistrationIdResolver registrationIdResolver = bootstrapContext + .getOrElseSupply(ClientRegistrationIdResolver.class, () -> request -> clientRegistrationId); + + ClientHttpRequestInterceptor interceptor = bootstrapContext.getOrElseSupply( + ClientHttpRequestInterceptor.class, + () -> buildInterceptor(authorizedClientManager, registrationIdResolver)); + + factory.addInterceptor(interceptor); + } + + static String requireClientRegistrationId(String clientRegistrationId) { + if (!StringUtils.hasText(clientRegistrationId)) { + throw new IllegalStateException("'" + ConfigClientOAuth2Properties.PREFIX + ".enabled' is true but '" + + ConfigClientOAuth2Properties.PREFIX + ".client-registration-id' is not set"); + } + return clientRegistrationId; + } + + static ClientRegistrationRepository buildClientRegistrationRepository(Binder binder, BindHandler bindHandler) { + OAuth2ClientProperties oauth2ClientProperties = binder + .bind("spring.security.oauth2.client", Bindable.of(OAuth2ClientProperties.class), bindHandler) + .orElseGet(OAuth2ClientProperties::new); + return buildClientRegistrationRepository(oauth2ClientProperties); + } + + static ClientRegistrationRepository buildClientRegistrationRepository( + OAuth2ClientProperties oauth2ClientProperties) { + oauth2ClientProperties.afterPropertiesSet(); + List registrations = new ArrayList<>( + new OAuth2ClientPropertiesMapper(oauth2ClientProperties).asClientRegistrations().values()); + return new InMemoryClientRegistrationRepository(registrations); + } + + static OAuth2AuthorizedClientManager buildAuthorizedClientManager( + ClientRegistrationRepository registrationRepository) { + OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .refreshToken() + .build(); + OAuth2AuthorizedClientService authorizedClientService = new InMemoryOAuth2AuthorizedClientService( + registrationRepository); + AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceOAuth2AuthorizedClientManager( + registrationRepository, authorizedClientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + return authorizedClientManager; + } + + static ClientHttpRequestInterceptor buildInterceptor(OAuth2AuthorizedClientManager manager, + ClientRegistrationIdResolver resolver) { + OAuth2ClientHttpRequestInterceptor interceptor = new OAuth2ClientHttpRequestInterceptor(manager); + interceptor.setClientRegistrationIdResolver(resolver); + return interceptor; + } + + } + +} diff --git a/spring-cloud-config-client/src/main/resources/META-INF/spring.factories b/spring-cloud-config-client/src/main/resources/META-INF/spring.factories index ef12ccda1c..482d2753fe 100644 --- a/spring-cloud-config-client/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-config-client/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ # Bootstrap components org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.config.client.ConfigServiceBootstrapConfiguration,\ +org.springframework.cloud.config.client.oauth2.ConfigClientOAuth2BootstrapConfiguration,\ org.springframework.cloud.config.client.DiscoveryClientConfigServiceBootstrapConfiguration # Environment PostProcessor