Repository: SAP/cloud-security-xsuaa-integration (java)

Issue:
NGPBUG-163948: create/forward correlation id for external requests to identity provider
See also [here](https://jtrack.wdf.sap.corp/browse/NGPBUG-163948) we need to set x-correlation-id header for each request, we initialize, e.g. towards identity provide in the token-client lib.

Either we set the `x-correlation-id` header with a new guid or if given, we get it from org.slf4j.MDC like that
```java
MDC.get(CORRELATION_ID); // public String CORRELATION_ID = "correlation_id";
```
See also here: https://github.com/SAP/cf-java-logging-support/blob/master/cf-java-logging-support-servlet/src/main/java/com/sap/hcp/cf/logging/servlet/filter/CorrelationIdFilter.java#L44-L53

See also here (SAP-internal): https://github.wdf.sap.corp/CPSecurity/xsuaa/blob/master/uaa-security/src/main/java/org/cloudfoundry/identity/uaa/zone/XSCorrelationIdLoggingFilter.java 

So, Application developers either need to intiialize the mapped diagnostic context:
```java
MDC.put(CORRELATION_ID, ((HttpServletRequest) request).getHeader("x-correlation-id"));
```
..or use this logging library: https://github.com/SAP/cf-java-logging-support

Further References:
- https://github.com/SAP-samples/cloud-bulletinboard-ads/tree/Documentation/LoggingTracing
- https://github.com/SAP-samples/cloud-bulletinboard-ads/blob/Documentation/Service2ServiceCommunication/Exercise_19_Transfer_CorrelationID.md

Pull request description:
Extend token-client library to set the correlation_id for subsequent/outgoing requests
closes #691 

Patch:
diff --git a/java-api/src/main/java/com/sap/cloud/security/servlet/MDCConstants.java b/java-api/src/main/java/com/sap/cloud/security/servlet/MDCConstants.java
new file mode 100644
index 00000000..8d898230
--- /dev/null
+++ b/java-api/src/main/java/com/sap/cloud/security/servlet/MDCConstants.java
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2018-2021 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors
+ * <p>
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.sap.cloud.security.servlet;
+
+
+/**
+ * Constants for Mapped Diagnostic Context.
+ */
+public class MDCConstants {
+
+    public static final String CORRELATION_ID = "correlation_id";
+    public static final String CORRELATION_HEADER = "X-CorrelationID";
+
+    private MDCConstants() {}
+}
diff --git a/java-api/src/main/java/com/sap/cloud/security/servlet/MDCHelper.java b/java-api/src/main/java/com/sap/cloud/security/servlet/MDCHelper.java
new file mode 100644
index 00000000..4ee2671e
--- /dev/null
+++ b/java-api/src/main/java/com/sap/cloud/security/servlet/MDCHelper.java
@@ -0,0 +1,40 @@
+/**
+ * SPDX-FileCopyrightText: 2018-2021 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors
+ * <p>
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.sap.cloud.security.servlet;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+import java.util.UUID;
+
+import static com.sap.cloud.security.servlet.MDCConstants.CORRELATION_ID;
+
+/**
+ * The Mapped Diagnostic Context helper class.
+ */
+public final class MDCHelper {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(MDCHelper.class);
+
+    private MDCHelper() {}
+
+    /**
+     * Gets correlation_id from MDC, if it is missing, new correlation_id will be created.
+     *
+     * @return the string of correlation_id
+     */
+    public static String getOrCreateCorrelationId(){
+        String correlationId = MDC.get(CORRELATION_ID);
+        if (correlationId == null || correlationId.isEmpty()){
+            correlationId = String.valueOf(UUID.randomUUID());
+            LOGGER.info("Correlation id (key={}) was not found in the MDC, generating a new one: {}", CORRELATION_ID, correlationId);
+        } else {
+            LOGGER.debug("Correlation id (key={}) from MDC: {}", CORRELATION_ID, correlationId);
+        }
+        return correlationId;
+    }
+}
diff --git a/pom.xml b/pom.xml
index 73bae383..c2f27b86 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,7 +60,7 @@
 		<!-- make sure that spring core and spring boot versions are compatible-->
 		<spring.boot.version>2.5.6</spring.boot.version>
 		<spring.core.version>5.3.13</spring.core.version>
-		<spring.security.version>5.5.3</spring.security.version>
+		<spring.security.version>5.6.0</spring.security.version>
 		<spring.security.oauth2.version>2.5.1.RELEASE</spring.security.oauth2.version>
 		<spring.security.jwt.version>1.1.1.RELEASE</spring.security.jwt.version>
 		<reactor.version>3.4.12</reactor.version>
diff --git a/samples/spring-security-hybrid-usage/README.md b/samples/spring-security-hybrid-usage/README.md
index 1495ab2c..8529473d 100644
--- a/samples/spring-security-hybrid-usage/README.md
+++ b/samples/spring-security-hybrid-usage/README.md
@@ -1,5 +1,6 @@
 # Description
 This spring boot application sample uses ```spring-security``` client library to validate jwt tokens issued by ```xsuaa``` service or by ```identity ``` service. On the one hand ```xsuaa``` service issues an access token and on the other hand ```identity``` service issues an oidc token. The tokens vary with regard to the information provided via token claims. In both cases the validated token is available of type [```Token```](https://github.com/SAP/cloud-security-xsuaa-integration/blob/master/java-api/src/main/java/com/sap/cloud/security/token/Token.java) via the ```SecurityContextHolder```.
+This sample also demonstrates usage of `CorrelationIdFilter` that adds correlation_id to MDC context that is used to enhance subsequent/outgoing requests with `X-CorrelationID` header. More information about logging filter library used can be found [here](https://github.com/SAP/cf-java-logging-support/wiki/Instrumenting-Servlets).
 
 # Coding
 This sample is using the [`spring-security`](/spring-security/) library, which is based on [Spring's Security](https://github.com/spring-projects/spring-security) project and runs on SAP Cloud Business Technology Platform. It integrates with [Spring Security OAuth 2.0 Resource Server](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver). The security configuration needs to configure jwt for authentication.
diff --git a/samples/spring-security-hybrid-usage/pom.xml b/samples/spring-security-hybrid-usage/pom.xml
index 76f52ba9..57c95052 100644
--- a/samples/spring-security-hybrid-usage/pom.xml
+++ b/samples/spring-security-hybrid-usage/pom.xml
@@ -52,6 +52,11 @@
 			<artifactId>httpclient</artifactId>
 			<version>${apache.httpclient.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>com.sap.hcp.cf.logging</groupId>
+			<artifactId>cf-java-logging-support-servlet</artifactId>
+			<version>3.5.2</version>
+		</dependency>
 
 		<!-- test dependencies-->
         <dependency>
diff --git a/samples/spring-security-hybrid-usage/src/main/java/sample/spring/security/Application.java b/samples/spring-security-hybrid-usage/src/main/java/sample/spring/security/Application.java
index f69a90fe..93fb741c 100644
--- a/samples/spring-security-hybrid-usage/src/main/java/sample/spring/security/Application.java
+++ b/samples/spring-security-hybrid-usage/src/main/java/sample/spring/security/Application.java
@@ -5,8 +5,14 @@
  */
 package sample.spring.security;
 
+import com.sap.hcp.cf.logging.servlet.filter.RequestLoggingFilter;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.Ordered;
+
+import javax.servlet.DispatcherType;
 
 @SpringBootApplication
 public class Application {
@@ -14,4 +20,15 @@ public class Application {
     public static void main(String[] args) {
         SpringApplication.run(Application.class, args);
     }
+
+    @Bean
+    public FilterRegistrationBean<RequestLoggingFilter> loggingFilter() {
+        FilterRegistrationBean<RequestLoggingFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new RequestLoggingFilter());
+        registrationBean.setName("request-logging");
+        registrationBean.addUrlPatterns("/*");
+        registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
+        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
+        return registrationBean;
+    }
 }
diff --git a/token-client/pom.xml b/token-client/pom.xml
index 211b4f43..cdb529ff 100644
--- a/token-client/pom.xml
+++ b/token-client/pom.xml
@@ -97,6 +97,11 @@
 			<artifactId>spring-security-oauth2-jose</artifactId>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>ch.qos.logback</groupId>
+			<artifactId>logback-classic</artifactId>
+			<scope>test</scope>
+		</dependency>
 	</dependencies>
 
 	<build>
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 c567d2b0..804a9d9d 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
@@ -6,6 +6,8 @@
 package com.sap.cloud.security.xsuaa.client;
 
 import com.sap.cloud.security.client.HttpClientFactory;
+import com.sap.cloud.security.servlet.MDCConstants;
+import com.sap.cloud.security.servlet.MDCHelper;
 import com.sap.cloud.security.xsuaa.Assertions;
 import com.sap.cloud.security.xsuaa.http.HttpHeaders;
 import com.sap.cloud.security.xsuaa.tokenflows.TokenCacheConfiguration;
@@ -73,6 +75,7 @@ public class DefaultOAuth2TokenService extends AbstractOAuth2TokenService {
 	@Override
 	protected OAuth2TokenResponse requestAccessToken(URI tokenEndpointUri, HttpHeaders headers,
 			Map<String, String> parameters) throws OAuth2ServiceException {
+		headers.withHeader(MDCConstants.CORRELATION_HEADER, MDCHelper.getOrCreateCorrelationId());
 		HttpPost httpPost = createHttpPost(tokenEndpointUri, headers, parameters);
 		LOGGER.debug("access token request {} - {}", headers, parameters);
 		return executeRequest(httpPost);
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 d8db9f35..ba6b35ba 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
@@ -5,6 +5,8 @@
  */
 package com.sap.cloud.security.xsuaa.client;
 
+import com.sap.cloud.security.servlet.MDCConstants;
+import com.sap.cloud.security.servlet.MDCHelper;
 import com.sap.cloud.security.xsuaa.http.HttpHeaders;
 import com.sap.cloud.security.xsuaa.tokenflows.TokenCacheConfiguration;
 import org.slf4j.Logger;
@@ -69,7 +71,7 @@ public class XsuaaOAuth2TokenService extends AbstractOAuth2TokenService {
 		// Create URI
 		UriComponentsBuilder builder = UriComponentsBuilder.fromUri(tokenEndpointUri);
 		URI requestUri = builder.build().encode().toUri();
-
+		headers.withHeader(MDCConstants.CORRELATION_HEADER, MDCHelper.getOrCreateCorrelationId());
 		org.springframework.http.HttpHeaders springHeaders = new org.springframework.http.HttpHeaders();
 		headers.getHeaders().forEach(h -> springHeaders.add(h.getName(), h.getValue()));
 


Test patch:
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenServiceTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenServiceTest.java
index 1b82121b..52717287 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenServiceTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenServiceTest.java
@@ -5,6 +5,11 @@
  */
 package com.sap.cloud.security.xsuaa.client;
 
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import com.sap.cloud.security.servlet.MDCHelper;
 import com.sap.cloud.security.xsuaa.http.HttpHeaders;
 import com.sap.cloud.security.xsuaa.http.HttpHeadersFactory;
 import com.sap.cloud.security.xsuaa.util.HttpClientTestFactory;
@@ -22,6 +27,8 @@ import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
 
 import java.io.IOException;
 import java.net.URI;
@@ -29,6 +36,7 @@ import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.util.Map;
 
+import static com.sap.cloud.security.servlet.MDCConstants.CORRELATION_ID;
 import static java.util.Collections.*;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -78,6 +86,27 @@ public class DefaultOAuth2TokenServiceTest {
 		assertThat(re.getTokenType()).isEqualTo(TOKEN_TYPE);
 	}
 
+	@Test
+	public void correlationIdProvisioning() throws IOException {
+		CloseableHttpResponse response = HttpClientTestFactory.createHttpResponse(VALID_JSON_RESPONSE);
+		when(mockHttpClient.execute(any(HttpPost.class))).thenReturn(response);
+
+		ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
+		Logger logger = (Logger) LoggerFactory.getLogger(MDCHelper.class);
+		listAppender.start();
+		logger.addAppender(listAppender);
+
+		requestAccessToken(emptyMap());
+		assertThat(listAppender.list.get(0).getLevel()).isEqualTo(Level.INFO);
+		assertThat(listAppender.list.get(0).getMessage()).contains("was not found in the MDC");
+
+		MDC.put(CORRELATION_ID, "my-correlation-id");
+		requestAccessToken(emptyMap());
+		assertThat(listAppender.list.get(1).getLevel()).isEqualTo(Level.DEBUG);
+		assertThat(listAppender.list.get(1).getArgumentArray()[1]).isEqualTo(("my-correlation-id"));
+		MDC.clear();
+	}
+
 	@Test
 	public void executeWithAdditionalParameters_putsParametersIntoPostBody() throws IOException {
 		ArgumentCaptor<HttpPost> httpPostCaptor = ArgumentCaptor.forClass(HttpPost.class);
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceClientCredentialsTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceClientCredentialsTest.java
index d0c2b184..3b37beb0 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceClientCredentialsTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceClientCredentialsTest.java
@@ -5,32 +5,42 @@
  */
 package com.sap.cloud.security.xsuaa.client;
 
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
 import com.sap.cloud.security.config.ClientCredentials;
 import com.sap.cloud.security.config.ClientIdentity;
+import com.sap.cloud.security.servlet.MDCHelper;
+import org.assertj.core.api.Assertions;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
 import org.springframework.http.HttpEntity;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.RestOperations;
 
+import java.io.IOException;
 import java.net.URI;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
+import static com.sap.cloud.security.servlet.MDCConstants.CORRELATION_ID;
 import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.*;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.*;
 
 @RunWith(MockitoJUnitRunner.class)
 public class XsuaaOAuth2TokenServiceClientCredentialsTest {
@@ -145,4 +155,26 @@ public class XsuaaOAuth2TokenServiceClientCredentialsTest {
 		assertThat(accessToken.getAccessToken(), is(responseMap.get(ACCESS_TOKEN)));
 	}
 
+	@Test
+	public void correlationIdProvisioning() throws IOException {
+		when(mockRestOperations.postForEntity(any(), any(), any())).thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
+		Logger logger = (Logger) LoggerFactory.getLogger(MDCHelper.class);
+		listAppender.start();
+		logger.addAppender(listAppender);
+
+		cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint,
+				clientIdentity, null, null, Collections.emptyMap(), false);
+		Assertions.assertThat(listAppender.list.get(0).getLevel()).isEqualTo(Level.INFO);
+		Assertions.assertThat(listAppender.list.get(0).getMessage()).contains("was not found in the MDC");
+
+		MDC.put(CORRELATION_ID, "my-correlation-id");
+		cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint,
+				clientIdentity, null, null, Collections.emptyMap(), false);
+		Assertions.assertThat(listAppender.list.get(1).getLevel()).isEqualTo(Level.DEBUG);
+		Assertions.assertThat(listAppender.list.get(1).getArgumentArray()[1]).isEqualTo(("my-correlation-id"));
+		MDC.clear();
+	}
+
 }
\ No newline at end of file