Repository: SAP/cloud-security-xsuaa-integration
Pull request #958 (2022-08-05 15:14:48)

add user-agent header to all http requests


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd029e69..a7ae3880 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,16 @@
 # Change Log
 All notable changes to this project will be documented in this file.
 
+## 2.13.1
+- [token-client]
+  - `DefaultHttpClientFactory` creates `CloseableHttpClient` with disabled redirects to avoid security vulnerabilities. :bangbang: For your custom `CloseableHttpClient` implementation make sure to disable redirects as well.
+  - all `TokenServices` and `TokenKeyServices` have been enhanced to add to all outgoing requests a `user-agent` header that contains value `token-client/x.x.x` where x.x.x is token-client version being used
+
+#### Dependency upgrades
+* Bump `spring.boot.version` from 2.7.1 to 2.7.2.
+* Bump https://github.com/SAP/btp-environment-variable-access from 0.3.1 to 0.4.0.
+* Bump reactor-core from 3.4.21 to 3.4.22
+
 ## 2.13.0
 - [env] 
   - uses https://github.com/SAP/btp-environment-variable-access (version 0.3.1), which supports access to service credentials in K8s/Kyma environment provisioned by [SAP BTP Service Operator](https://github.com/SAP/sap-btp-service-operator). Usage of service-catalog is no longer supported. With that there is no service-manager longer required to distinguish the service instance plan, when multiple xsuaa instances are bound (#855).
@@ -11,8 +21,8 @@ All notable changes to this project will be documented in this file.
 
 #### Dependency upgrades
 * Bump log4j2.version from 2.17.2 to 2.18.0
-* Bumps `spring.core.version` from 5.3.21 to 5.3.22.
-* Bumps [reactor-core](https://github.com/reactor/reactor-core) from 3.4.19 to 3.4.21.
+* Bump `spring.core.version` from 5.3.21 to 5.3.22.
+* Bump [reactor-core](https://github.com/reactor/reactor-core) from 3.4.19 to 3.4.21.
 
 
 **Full Changelog**: https://github.com/SAP/cloud-security-xsuaa-integration/compare/2.12.3...2.13.0
diff --git a/pom.xml b/pom.xml
index e46353e6..55cdfbd6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,7 +62,7 @@
 		<spring.security.version>5.7.2</spring.security.version>
 		<spring.security.oauth2.version>2.5.2.RELEASE</spring.security.oauth2.version>
 		<spring.security.jwt.version>1.1.1.RELEASE</spring.security.jwt.version>
-		<reactor.version>3.4.21</reactor.version>
+		<reactor.version>3.4.22</reactor.version>
 		<log4j2.version>2.18.0</log4j2.version>
 		<slf4j.api.version>1.7.36</slf4j.api.version> <!--see also here http://www.slf4j.org/faq.html#changesInVersion18 -->
 		<org.json.version>20220320</org.json.version>
@@ -79,7 +79,7 @@
 		<hamcrest.version>1.3</hamcrest.version>
 		<mockito.version>3.12.4</mockito.version>
 		<assertj.version>3.23.1</assertj.version>
-		<reactor.test.version>3.4.21</reactor.test.version>
+		<reactor.test.version>3.4.22</reactor.test.version>
 		<wiremock.version>2.33.2</wiremock.version>
 		<javax.annotation.version>1.3.2</javax.annotation.version>
 		<spotbugs.annotations.version>4.7.1</spotbugs.annotations.version>
diff --git a/spring-security/Migration_SpringXsuaaProjects.md b/spring-security/Migration_SpringXsuaaProjects.md
index 7b2514fc..e4e4aecc 100644
--- a/spring-security/Migration_SpringXsuaaProjects.md
+++ b/spring-security/Migration_SpringXsuaaProjects.md
@@ -230,7 +230,7 @@ application locally make sure it is still working and finally test the applicati
 
 
 ## Issues
-In case you face issues to apply the migration steps check this [troubleshoot](README.md#troubleshoot) for known issues and how to file the issue. With that you can help us to improve this migration guide!
+In case you face issues with the migration steps check this [troubleshoot guide](/spring-security/README.md#troubleshoot) for known issues and file a new github issue in case youe problem is not yet described in the section. With that you can help us to improve this migration guide!
 
 ## Samples
 - [Sample](/samples/spring-security-hybrid-usage)    
diff --git a/spring-security/README.md b/spring-security/README.md
index 59eda364..f5d8b34d 100644
--- a/spring-security/README.md
+++ b/spring-security/README.md
@@ -294,11 +294,26 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 ```
 
 Finally, you need do re-deploy your application for the changes to take effect.
-### Known pitfalls
+### Common pitfalls
 
 #### Configuration property name vcap.services.<<xsuaa instance name>>.credentials is not valid
 We recognized that this error is raised, when your instance name contains upper cases.
 
+#### Local setup fails with "APPLICATION FAILED TO START"
+When you're trying to run the application locally, but application fails to start with the following error message:
+```log
+org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.core.convert.converter.Converter<org.springframework.security.oauth2.jwt.Jwt, org.springframework.security.authentication.AbstractAuthenticationToken>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}
+
+***************************
+APPLICATION FAILED TO START
+***************************
+Field authConverter in com.sap.cloud.test.SecurityConfiguration required a bean of type 'org.springframework.core.convert.converter.Converter' that could not be found.
+```
+Make sure that you have defined the following mandatory attribute in the service configuration (VCAP_SERVICES env variable or application.yaml or application.properties)
+- xsappname
+
+:bulb: Example of minimal application configuration [application.yml](/samples/spring-security-hybrid-usage/src/test/resources/application.yml) for local setup.
+
 ## Samples
 - [Sample](/samples/spring-security-hybrid-usage)    
 demonstrating how to leverage ``spring-security`` library to secure a Spring Boot web application with tokens issued by xsuaa or identity service. Furthermore it documents how to implement SpringWebMvcTests using `java-security-test` library.
diff --git a/token-client/pom.xml b/token-client/pom.xml
index 9a85a4e2..6d529724 100644
--- a/token-client/pom.xml
+++ b/token-client/pom.xml
@@ -117,6 +117,22 @@
     </dependencies>
 
 	<build>
+		<resources>
+			<resource>
+				<directory>src/main/resources</directory>
+				<filtering>true</filtering>
+				<includes>
+					<include>**/token-client.properties</include>
+				</includes>
+			</resource>
+			<resource>
+				<directory>src/main/resources</directory>
+				<filtering>false</filtering>
+				<excludes>
+					<exclude>**/token-client.properties</exclude>
+				</excludes>
+			</resource>
+		</resources>
 		<plugins>
 			<plugin>
 				<groupId>org.owasp</groupId>
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java
index 4bde6d8f..a7d2eb4d 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java
@@ -8,6 +8,7 @@ package com.sap.cloud.security.xsuaa.client;
 import com.sap.cloud.security.client.HttpClientFactory;
 import com.sap.cloud.security.xsuaa.Assertions;
 import com.sap.cloud.security.xsuaa.util.HttpClientUtil;
+import org.apache.http.HttpHeaders;
 import org.apache.http.HttpStatus;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
@@ -45,6 +46,9 @@ public class DefaultOAuth2TokenKeyService implements OAuth2TokenKeyService {
 		if (zoneId != null) {
 			request.addHeader(X_ZONE_UUID, zoneId);
 		}
+		request.addHeader(HttpHeaders.USER_AGENT, HttpClientUtil.getUserAgent());
+
+		LOGGER.debug("Executing token key retrieval GET request to {} with headers: {} ", tokenKeysEndpointUri, request.getAllHeaders());
 		try (CloseableHttpResponse response = httpClient.execute(request)) {
 			String bodyAsString = HttpClientUtil.extractResponseBodyAsString(response);
 			int statusCode = response.getStatusLine().getStatusCode();
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenService.java
index 85ad58a9..301bacec 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenService.java
@@ -31,6 +31,8 @@ import java.util.Map;
 import java.util.stream.Collectors;
 
 import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.*;
+import static org.apache.http.HttpHeaders.USER_AGENT;
+
 
 public class DefaultOAuth2TokenService extends AbstractOAuth2TokenService {
 
@@ -84,6 +86,7 @@ public class DefaultOAuth2TokenService extends AbstractOAuth2TokenService {
 	}
 
 	private OAuth2TokenResponse executeRequest(HttpPost httpPost) throws OAuth2ServiceException {
+		httpPost.addHeader(USER_AGENT, HttpClientUtil.getUserAgent());
 		LOGGER.debug("Requesting access token from url {} with headers {}", httpPost.getURI(),
 				httpPost.getAllHeaders());
 		try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationService.java
index 4fcf09b4..704600f2 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationService.java
@@ -20,6 +20,8 @@ import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.net.URI;
 
+import org.apache.http.HttpHeaders;
+
 /**
  * https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationRequest
  */
@@ -48,6 +50,8 @@ public class DefaultOidcConfigurationService implements OidcConfigurationService
 		Assertions.assertNotNull(discoveryEndpointUri, "discoveryEndpointUri must not be null!");
 
 		HttpUriRequest request = new HttpGet(discoveryEndpointUri);
+		request.addHeader(HttpHeaders.USER_AGENT, HttpClientUtil.getUserAgent());
+
 		try (CloseableHttpResponse response = httpClient.execute(request)) {
 			String bodyAsString = HttpClientUtil.extractResponseBodyAsString(response);
 			int statusCode = response.getStatusLine().getStatusCode();
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
index 5ec507bf..6238694d 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
@@ -6,6 +6,7 @@
 package com.sap.cloud.security.xsuaa.client;
 
 import com.sap.cloud.security.xsuaa.Assertions;
+import com.sap.cloud.security.xsuaa.util.HttpClientUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.*;
@@ -39,6 +40,7 @@ public class SpringOAuth2TokenKeyService implements OAuth2TokenKeyService {
 			// create headers
 			HttpHeaders headers = new HttpHeaders();
 			headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
+			headers.set(HttpHeaders.USER_AGENT, HttpClientUtil.getUserAgent());
 			if (zoneId != null) {
 				headers.set(X_ZONE_UUID, zoneId);
 			}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationService.java
index afc8a0ad..96971d94 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationService.java
@@ -9,8 +9,8 @@ import javax.annotation.Nonnull;
 
 import java.net.URI;
 
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
+import com.sap.cloud.security.xsuaa.util.HttpClientUtil;
+import org.springframework.http.*;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestOperations;
 
@@ -29,7 +29,10 @@ public class SpringOidcConfigurationService implements OidcConfigurationService
 			throws OAuth2ServiceException {
 		Assertions.assertNotNull(discoveryEndpointUri, "discoveryEndpointUri must not be null!");
 		try {
-			ResponseEntity<String> response = restOperations.getForEntity(discoveryEndpointUri, String.class);
+			HttpHeaders headers = new HttpHeaders();
+			headers.set(HttpHeaders.USER_AGENT, HttpClientUtil.getUserAgent());
+
+			ResponseEntity<String> response = restOperations.exchange(discoveryEndpointUri, HttpMethod.GET, new HttpEntity(headers), String.class);
 			if (HttpStatus.OK.value() == response.getStatusCode().value()) {
 				return new DefaultOidcConfigurationService.OidcEndpointsProvider(response.getBody());
 			} else {
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java
index 8de58620..79d2cd64 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java
@@ -8,6 +8,7 @@ package com.sap.cloud.security.xsuaa.client;
 import com.sap.cloud.security.servlet.MDCHelper;
 import com.sap.cloud.security.xsuaa.http.HttpHeaders;
 import com.sap.cloud.security.xsuaa.tokenflows.TokenCacheConfiguration;
+import com.sap.cloud.security.xsuaa.util.HttpClientUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.http.HttpEntity;
@@ -73,6 +74,7 @@ public class XsuaaOAuth2TokenService extends AbstractOAuth2TokenService {
 		org.springframework.http.HttpHeaders springHeaders = new org.springframework.http.HttpHeaders();
 		headers.getHeaders().forEach(h -> springHeaders.add(h.getName(), h.getValue()));
 		springHeaders.add(MDCHelper.CORRELATION_HEADER, MDCHelper.getOrCreateCorrelationId());
+		springHeaders.add(org.springframework.http.HttpHeaders.USER_AGENT, HttpClientUtil.getUserAgent());
 		// Create entity
 		HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(copyIntoForm(parameters),
 				springHeaders);
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/HttpClientUtil.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/HttpClientUtil.java
index f4bc7e93..744420bc 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/HttpClientUtil.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/HttpClientUtil.java
@@ -10,7 +10,9 @@ import org.apache.http.HttpResponse;
 
 import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.util.Properties;
 import java.util.stream.Collectors;
 
 public class HttpClientUtil {
@@ -25,4 +27,15 @@ public class HttpClientUtil {
 				.lines().collect(Collectors.joining(System.lineSeparator()));
 	}
 
+	public static String getUserAgent() {
+		Properties props = new Properties();
+		InputStream stream = HttpClientUtil.class.getResourceAsStream("/token-client.properties");
+		try {
+			props.load(stream);
+			return props.getProperty("artifactId") + "/" + props.getProperty("version");
+		} catch (IOException | NullPointerException | IllegalArgumentException e) {
+			return "token-client/0.0.0";
+		}
+	}
+
 }
diff --git a/token-client/src/main/resources/token-client.properties b/token-client/src/main/resources/token-client.properties
new file mode 100644
index 00000000..7303e137
--- /dev/null
+++ b/token-client/src/main/resources/token-client.properties
@@ -0,0 +1,2 @@
+artifactId=${project.artifactId}
+version=${project.version}
\ No newline at end of file


Test patch:
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationServiceTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationServiceTest.java
index 988e9d38..2d015a86 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationServiceTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOidcConfigurationServiceTest.java
@@ -62,7 +62,7 @@ public class DefaultOidcConfigurationServiceTest {
 
 	@Test
 	public void retrieveEndpoints_badRequest_throwsException() throws IOException {
-		String errorDescription = "Something wen't wrong";
+		String errorDescription = "Something went wrong";
 		CloseableHttpResponse response = HttpClientTestFactory
 				.createHttpResponse(errorDescription, HttpStatus.SC_BAD_REQUEST);
 		when(httpClientMock.execute(any())).thenReturn(response);
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationServiceTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationServiceTest.java
index 85b3aa07..e3c1deb8 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationServiceTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/SpringOidcConfigurationServiceTest.java
@@ -5,27 +5,26 @@
  */
 package com.sap.cloud.security.xsuaa.client;
 
-import static com.sap.cloud.security.xsuaa.client.OidcConfigurationService.DISCOVERY_ENDPOINT_DEFAULT;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
-
-import java.io.IOException;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-
 import org.apache.commons.io.IOUtils;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mockito;
+import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.client.RestOperations;
 
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import static com.sap.cloud.security.xsuaa.client.OidcConfigurationService.DISCOVERY_ENDPOINT_DEFAULT;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
 public class SpringOidcConfigurationServiceTest {
 	public static final URI CONFIG_ENDPOINT_URI = URI.create("https://sub.myauth.com" + DISCOVERY_ENDPOINT_DEFAULT);
 
@@ -51,16 +50,16 @@ public class SpringOidcConfigurationServiceTest {
 	}
 
 	@Test
-	public void retrieveEndpoints_parameterIsNull_throwsException() throws OAuth2ServiceException {
+	public void retrieveEndpoints_parameterIsNull_throwsException() {
 		assertThatThrownBy(() -> retrieveEndpoints(null))
 				.isInstanceOf(IllegalArgumentException.class);
 	}
 
 	@Test
 	public void retrieveEndpoints_badRequest_throwsException() {
-		String errorDescription = "Something wen't wrong";
+		String errorDescription = "Something went wrong";
 		mockResponse(errorDescription, HttpStatus.BAD_REQUEST);
-		assertThatThrownBy(() -> retrieveEndpoints())
+		assertThatThrownBy(this::retrieveEndpoints)
 				.isInstanceOf(OAuth2ServiceException.class)
 				.hasMessageContaining(errorDescription);
 	}
@@ -72,7 +71,7 @@ public class SpringOidcConfigurationServiceTest {
 		retrieveEndpoints();
 
 		Mockito.verify(restOperationsMock, times(1))
-				.getForEntity(CONFIG_ENDPOINT_URI, String.class);
+				.exchange(eq(CONFIG_ENDPOINT_URI), eq(HttpMethod.GET), any(), eq(String.class));
 	}
 
 	@Test
@@ -81,9 +80,9 @@ public class SpringOidcConfigurationServiceTest {
 
 		OAuth2ServiceEndpointsProvider result = retrieveEndpoints();
 
-		assertThat(result.getTokenEndpoint().toString()).isEqualTo("http://localhost/oauth/token");
-		assertThat(result.getJwksUri().toString()).isEqualTo("http://localhost/token_keys");
-		assertThat(result.getAuthorizeEndpoint().toString()).isEqualTo("http://localhost/oauth/authorize");
+		assertThat(result.getTokenEndpoint()).hasToString("http://localhost/oauth/token");
+		assertThat(result.getJwksUri()).hasToString("http://localhost/token_keys");
+		assertThat(result.getAuthorizeEndpoint()).hasToString("http://localhost/oauth/authorize");
 	}
 
 	private void mockResponse() {
@@ -92,7 +91,7 @@ public class SpringOidcConfigurationServiceTest {
 
 	private void mockResponse(String responseAsString, HttpStatus httpStatus) {
 		ResponseEntity<String> stringResponseEntity = new ResponseEntity<>(responseAsString, httpStatus);
-		when(restOperationsMock.getForEntity(any(URI.class), eq(String.class)))
+		when(restOperationsMock.exchange(any(URI.class), eq(HttpMethod.GET), any(), eq(String.class)))
 				.thenReturn(stringResponseEntity);
 	}