Repository: SAP/cloud-security-xsuaa-integration
Pull request #546 (2021-06-21 18:14:51)

enable e2e test for mtls
- [x] adapt token request to use X.509
- [x] add client secret based Test for tokenFlows 
- [x] refactor base test to create service instances once per test class


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ba9312d..3fe17332 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,17 @@
 # Change Log
 All notable changes to this project will be documented in this file.
 
+## 2.10.1 and 0.3.1 [BETA]
+#### Dependency upgrades
+- org.springframework.boot:spring-boot 2.5.0 --> 2.5.2
+- org.springframework:spring-core 5.3.7 --> 5.3.8
+- org.springframework.security:spring-security-oauth2-jose 5.5.0 --> 5.5.1
+- org.springframework.security:spring-security-oauth2-resource-server 5.5.0 --> 5.5.1
+- org.springframework.security:spring-security-oauth2-jose 5.5.0 --> 5.5.1
+- org.springframework.security:spring-boot-starter-test 5.5.0 --> 5.5.1
+- org.springframework.security.oauth:spring-security-oauth2 2.5.0.RELEASE --> 2.5.1.RELEASE
+
+
 ## 2.10.0 and 0.3.0 [BETA]
 - [java-api] provides `ClientIdentity` with 2 implementations: `ClientCredentials` and `ClientCertificate`
 - [token-client] 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..abdea5a6
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,106 @@
+# Contributing to cloud-security-xsuaa-integration client libraries
+
+If you want to contribute to cloud-security-xsuaa-integration you're more than Welcome! Please read this document to understand what you can do:
+ * [Analyze Issues](#analyze-issues)
+ * [Report an Issue](#report-an-issue)
+ * [Contribute Code](#contribute-code)
+
+## Analyze Issues
+
+Analyzing issues can be a lot of effort. Any help is appreciated!
+Go to [the Github issue tracker](https://github.com/SAP/cloud-security-xsuaa-integration/issues?state=open) and find an open issue which needs additional work or a bugfix.
+
+Additional work may be: further information, or it might be a hint that helps understand the issue. Maybe you can even find and [contribute](#contribute-code) a bugfix?
+
+
+## Report an Issue
+
+If you find a bug in cloud-security-xsuaa-integration code feel free to report it.
+To avoid back and forth messages and shorten the bug fix time provide a well-described issue description. Please follow the [checklist](#Issue-Report-Checklist) below.
+
+Once you have familiarized with the checklist, you can go to the [Github issue tracker](https://github.com/SAP/cloud-security-xsuaa-integration/issues/new) to report the issue.
+
+> For consultation questions search first in [SAP Jam](https://jam4.sapjam.com/groups/DRuoC97ApSanbbXx20g4kb/forums) or [GitHub Consultation Questions](https://github.com/SAP/cloud-security-xsuaa-integration/issues?q=label%3Aconsultation+) for already replied questions. If you still don't find an answer create a [new issue](https://github.com/SAP/cloud-security-xsuaa-integration/issues/new) with `consultation` label.
+
+### Issue Report Checklist
+
+ * Real, current bug
+ * Not a duplicate
+ * Not covered in "Common Pitfalls" section of corresponding module's README.md e.g. [java-security#common-pitfalls](https://github.com/SAP/cloud-security-xsuaa-integration/tree/master/java-security#common-pitfalls)
+ * Reproducible
+ * Good summary
+ * Well-documented
+    * log level increased to `DEBUG` debug logs provided
+    * POM provided
+    * dependency tree provided
+    * code snippet provided
+
+
+### Requirements for an Issue report
+
+These seven requirements are mandatory for a good Issue report:
+1. **Only real bugs**: make sure to only report real bugs! Do not report:
+   * issues caused by application code or any code outside cloud-security-xsuaa-integration library.
+   * something you do not get to work properly. Use a support channels mentioned above to request help.
+2. No duplicate: you have searched the issue tracker to make sure the bug has not yet been reported
+3. Good summary: the summary should be specific to the issue
+4. Current bug: the bug can be reproduced in the most current version (Deprecated modules, classes, methods etc. are not supported)
+5. Reproducible bug: there are clear steps to reproduce the issue. 
+6. Precise description:
+   * precisely state the expected and the actual behavior
+   * check troubleshoot guidelines for [Java applications](https://github.com/SAP/cloud-security-xsuaa-integration/tree/master/java-security#troubleshoot) 
+   * check troubleshoot guidelines for [Spring applications](https://github.com/SAP/cloud-security-xsuaa-integration/tree/master/spring-xsuaa#troubleshoot)  
+7. Only one bug per report: open different tickets for different issues
+
+Please report bugs in English, so all users can understand them.
+
+
+## Contribute Code
+
+You are welcome to contribute code to cloud-security-xsuaa-integration in order to fix bugs or to implement new features.
+
+There are two important things to know:
+
+1.  You must be aware of the Apache License (which describes contributions) and **agree to the Developer Certificate of Origin**. This is common practice in all major Open Source projects. To make this process as simple as possible, we are using *[CLA assistant](https://cla-assistant.io/)*. CLA assistant is an open source tool that integrates with GitHub very well and enables a one-click-experience for accepting the DCO. See the respective section below for details.
+2.  There are **several requirements regarding code style, quality, and product standards** that need to be adhered to. The [Contribution Content Guidelines](#Contribution-Content-Guidelines) section below describes that in more details.
+
+
+### Developer Certificate of Origin (DCO)
+
+Due to legal reasons, contributors will be asked to accept a DCO before they submit the first pull request to this project. SAP uses [the standard DCO text of the Linux Foundation](https://developercertificate.org/).  
+This happens in an automated fashion during the submission process: the CLA assistant tool will add a comment to the pull request. Click it to check the DCO, then accept it on the following screen. CLA assistant will save this decision for upcoming contributions.
+
+This DCO replaces the previously used CLA ("Contributor License Agreement") as well as the "Corporate Contributor License Agreement" with new terms which are well-known standards and hence easier to approve by legal departments. Contributors who had already accepted the CLA in the past may be asked once to accept the new DCO.
+
+
+### Contribution Content Guidelines
+
+Contributed content can be accepted if it:
+
+1. is useful to improve cloud-security-xsuaa-integration library (explained above)
+2. follows the applicable guidelines and standards
+
+Some of the most important rules are listed below:
+
+-   Follow a clean coding style principles that complies with JAVA naming convention
+-   Apply maven formatter goal `mvn formatter:format` when you're ready to push your code
+-   Provide full test coverage for your implementation
+-   Comment your code where it gets non-trivial and keep the JavaDocs up to date
+-   Use the most restrictive access level possible for any given member
+-   **Do NOT** do any incompatible changes
+
+### How to contribute - the Process
+
+1.  Make sure the change is useful (e.g. a bugfix or a useful feature). Recommended way is to propose it in a GitHub issue
+2.  Create a branch forking the cloud-security-xsuaa-integration repository and implement your change
+3.  Commit and push your changes to that branch
+    -   If you have several commits, squash them into one (see [this explanation](http://davidwalsh.name/squash-commits-git))
+
+5.  If your change fixes an issue reported at GitHub, add the following line to the commit message:
+    - ```Fixes https://github.com/SAP/cloud-security-xsuaa-integration/issues/(issueNumber)```
+    - Do NOT add a colon after "Fixes" - this prevents automatic closing.
+	- When your pull request number is known (e.g. because you enhance a pull request after a code review), you can also add the line ```Closes https://github.com/SAP/cloud-security-xsuaa-integration/issues/pull/(pullRequestNumber)```
+6.  Create a Pull Request to https://github.com/SAP/cloud-security-xsuaa-integration/pulls
+7.  Follow the link posted by the CLA assistant to your pull request and accept the Developer Certificate of Origin, as described in detail above.
+8.  Wait for our code review and approval, possibly enhancing your change on request
+9.  Once the change has been approved it will be merged into master and pull request will be closed (feel free to delete the now obsolete branch)
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 6548ca48..2ccb7aae 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,10 +58,10 @@
 		<maven.compiler.source>1.8</maven.compiler.source>
 		<maven.source.plugin.version>3.1.0</maven.source.plugin.version>
 		<!-- make sure that spring core and spring boot versions are compatible-->
-		<spring.boot.version>2.5.0</spring.boot.version>
-		<spring.core.version>5.3.7</spring.core.version>
-		<spring.security.version>5.5.0</spring.security.version>
-		<spring.security.oauth2.version>2.5.0.RELEASE</spring.security.oauth2.version>
+		<spring.boot.version>2.5.2</spring.boot.version>
+		<spring.core.version>5.3.8</spring.core.version>
+		<spring.security.version>5.5.1</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.6</reactor.version>
 		<log4j.version>2.14.1</log4j.version>
diff --git a/samples/spring-security-basic-auth/pom.xml b/samples/spring-security-basic-auth/pom.xml
index a4d05f0c..f405b658 100644
--- a/samples/spring-security-basic-auth/pom.xml
+++ b/samples/spring-security-basic-auth/pom.xml
@@ -8,7 +8,7 @@
 	<parent>
 		<groupId>org.springframework.boot</groupId>
 		<artifactId>spring-boot-starter-parent</artifactId>
-		<version>2.5.0</version>
+		<version>2.5.2</version>
 		<relativePath/>
 	</parent>
 
diff --git a/samples/spring-security-hybrid-usage/pom.xml b/samples/spring-security-hybrid-usage/pom.xml
index e6ce78d4..19ffd160 100644
--- a/samples/spring-security-hybrid-usage/pom.xml
+++ b/samples/spring-security-hybrid-usage/pom.xml
@@ -10,7 +10,7 @@
 	<parent>
 		<groupId>org.springframework.boot</groupId>
 		<artifactId>spring-boot-starter-parent</artifactId>
-		<version>2.5.0</version>
+		<version>2.5.2</version>
 		<relativePath/>
 	</parent>
 
diff --git a/samples/spring-security-xsuaa-usage/pom.xml b/samples/spring-security-xsuaa-usage/pom.xml
index 5a200111..a51827f1 100644
--- a/samples/spring-security-xsuaa-usage/pom.xml
+++ b/samples/spring-security-xsuaa-usage/pom.xml
@@ -10,7 +10,7 @@
 	<parent>
 		<groupId>org.springframework.boot</groupId>
 		<artifactId>spring-boot-starter-parent</artifactId>
-		<version>2.5.0</version>
+		<version>2.5.2</version>
 		<relativePath/>
 	</parent>
 
diff --git a/samples/spring-webflux-security-xsuaa-usage/pom.xml b/samples/spring-webflux-security-xsuaa-usage/pom.xml
index 31bd518e..e4a1e60e 100644
--- a/samples/spring-webflux-security-xsuaa-usage/pom.xml
+++ b/samples/spring-webflux-security-xsuaa-usage/pom.xml
@@ -8,7 +8,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.5.0</version>
+        <version>2.5.2</version>
         <relativePath/>
     </parent>
 
diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java b/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java
index 2e452c23..303bba27 100644
--- a/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java
+++ b/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java
@@ -60,7 +60,7 @@ class XsuaaTokenFlowAutoConfiguration {
 	@Bean
 	@Conditional({ PropertyConditions.class })
 	public XsuaaTokenFlows xsuaaTokenFlows(RestOperations xsuaaRestOperations) {
-		logger.debug("auto-configures XsuaaTokenFlows.");
+		logger.debug("auto-configures XsuaaTokenFlows using {} based restOperations", xsuaaConfig.getClientIdentity().isCertificateBased() ? "certificate":"client secret");
 		OAuth2ServiceEndpointsProvider endpointsProvider = new XsuaaDefaultEndpoints(xsuaaConfig);
 		ClientIdentity clientIdentity = xsuaaConfig.getClientIdentity();
 		OAuth2TokenService oAuth2TokenService = new XsuaaOAuth2TokenService(xsuaaRestOperations);
diff --git a/spring-xsuaa/pom.xml b/spring-xsuaa/pom.xml
index ef2dec0a..bf83e12a 100644
--- a/spring-xsuaa/pom.xml
+++ b/spring-xsuaa/pom.xml
@@ -56,6 +56,11 @@
 			<groupId>${project.groupId}</groupId>
 			<artifactId>api</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>com.sap.cloud.security</groupId>
+			<artifactId>java-api</artifactId>
+			<version>${project.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>${project.groupId}</groupId>
 			<artifactId>token-client</artifactId>
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaTokenFlowAutoConfiguration.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaTokenFlowAutoConfiguration.java
index 76d19c3c..ea036816 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaTokenFlowAutoConfiguration.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaTokenFlowAutoConfiguration.java
@@ -58,7 +58,7 @@ public class XsuaaTokenFlowAutoConfiguration {
 	@ConditionalOnMissingBean
 	public XsuaaTokenFlows xsuaaTokenFlows(RestOperations xsuaaRestOperations,
 			XsuaaServiceConfiguration xsuaaServiceConfiguration) {
-		logger.debug("auto-configures XsuaaTokenFlows using restOperations of type: {}", xsuaaRestOperations);
+		logger.debug("auto-configures XsuaaTokenFlows using {} based restOperations", xsuaaServiceConfiguration.getClientIdentity().isCertificateBased() ? "certificate":"client secret");
 		OAuth2ServiceEndpointsProvider endpointsProvider = new XsuaaDefaultEndpoints(
 				xsuaaServiceConfiguration);
 		ClientIdentity clientCredentials = xsuaaServiceConfiguration.getClientIdentity();
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeader.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeader.java
index 78b82443..0fd39a22 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeader.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeader.java
@@ -40,4 +40,9 @@ public class HttpHeader {
 	public int hashCode() {
 		return Objects.hash(getName(), getValue());
 	}
+
+	@Override
+	public String toString() {
+		return "\"" + name + ": " + value + "\"";
+	}
 }
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java
index d5476205..6abe9bdf 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java
@@ -52,8 +52,9 @@ public class HttpHeaders {
 
 	@Override
 	public String toString() {
-		return "HttpHeaders{" +
-				"headers=" + headers +
-				'}';
+		return "HttpHeaders: [ " +
+				headers.stream().map(HttpHeader::toString)
+						.collect(Collectors.joining(", ")) +
+				" ]";
 	}
 }


Test patch:
diff --git a/samples/deploy_and_test.py b/samples/deploy_and_test.py
index 9745bf60..a8c574d8 100755
--- a/samples/deploy_and_test.py
+++ b/samples/deploy_and_test.py
@@ -3,6 +3,8 @@
 # SPDX-License-Identifier: Apache-2.0
 import abc
 import distutils
+import http
+import ssl
 import subprocess
 import urllib.request
 from urllib.parse import urlencode
@@ -69,39 +71,42 @@ credentials = Credentials()
 
 # Abstract base class for sample app tests classes 
 class SampleTest(abc.ABC, unittest.TestCase):
+    cf_app = None
+    api_access = None
+    ias_access = None
 
+    @classmethod
     @abc.abstractmethod
-    def get_app(self):
+    def get_app(cls):
         """Should return the sample app that should be tested """
-        self.skipTest('Dont run abstract base class')
-        return CFApp(None, None)
+        cls.skipTest('Dont run abstract base class')
+        return cls.cf_app
 
-    def setUp(self):
+    @classmethod
+    def setUpClass(cls):
         vars_file = open('./vars.yml')
-        self.vars_parser = VarsParser(vars_file.read())
+        cls.vars_parser = VarsParser(vars_file.read())
         vars_file.close()
-        self.cf_apps = CFApps()
-        self.__deployed_app = None
-        self.__api_access = None
-        self.__ias_access = None
-        self.credentials = credentials
-        self.cf_app = self.get_app()
-        self.cf_app.deploy()
+        cls.cf_apps = CFApps()
+        cls.__deployed_app = None
+        cls.get_app(cls).deploy()
+        cls.credentials = credentials
         time.sleep(2)  # waiting for deployed apps to be available
 
-    def tearDown(self):
-        self.cf_app.delete()
-        if self.__api_access is not None:
-            self.__api_access.delete()
-        if self.__ias_access is not None:
-            self.__ias_access.delete()
+    @classmethod
+    def tearDownClass(cls):
+        cls.cf_app.delete()
+        if cls.api_access is not None:
+            cls.api_access.delete()
+        if cls.ias_access is not None:
+            cls.ias_access.delete()
 
     def add_user_to_role(self, role):
         logging.info('Assigning role collection {} for user {}'.format(role, self.credentials.username))
         user = self.__get_api_access().get_user_by_username(self.credentials.username)
         user_id = user.get('id')
         resp = self.__get_api_access().add_user_to_group(user_id, role)
-        if not resp.is_ok:
+        if not resp.is_ok and resp.status != 409:  # 409 is returned when role is already assigned to the user
             logging.error(
                 "Could not set '{}' role to user '{}'. Error: {} - {}".format(role, user.get('userName'), resp.status,
                                                                               resp.body))
@@ -136,17 +141,28 @@ class SampleTest(abc.ABC, unittest.TestCase):
 
     def get_token(self):
         deployed_app = self.get_deployed_app()
-        logging.info('GET xsuaa token to {} for user {} ({}, {})'.format(deployed_app.xsuaa_service_url,
+        logging.info('GET xsuaa token to {} for user {} (credential-type = {}, clientid = {}, clientsecret = {})'.format(deployed_app.xsuaa_service_url,
                                                                          self.credentials.username,
+                                                                         deployed_app.credential_type,
                                                                          deployed_app.clientid,
                                                                          deployed_app.clientsecret))
-        return HttpUtil().get_token(
-            xsuaa_service_url=deployed_app.xsuaa_service_url,
-            clientid=deployed_app.clientid,
-            clientsecret=deployed_app.clientsecret,
-            grant_type='password',
-            username=self.credentials.username,
-            password=self.credentials.password + self.__get_2factor_auth_code())
+        if deployed_app.credential_type == 'x509':
+            body = HttpUtil.encode_request_body(self, clientid=deployed_app.clientid,
+                                         grant_type='password',
+                                         username=self.credentials.username,
+                                         password=self.credentials.password + self.__get_2factor_auth_code())
+            return HttpUtil.post_request_x509(self, url=deployed_app.xsuaa_cert_url,
+                                       data=body,
+                                       certificate=deployed_app.certificate,
+                                       key=deployed_app.key)
+        else:
+            return HttpUtil().get_token(
+                xsuaa_service_url=deployed_app.xsuaa_service_url,
+                clientid=deployed_app.clientid,
+                clientsecret=deployed_app.clientsecret,
+                grant_type='password',
+                username=self.credentials.username,
+                password=self.credentials.password + self.__get_2factor_auth_code())
 
     def get_id_token(self):
         deployed_app = self.get_deployed_app()
@@ -167,18 +183,20 @@ class SampleTest(abc.ABC, unittest.TestCase):
             exit()
         return id_token
 
-    def __get_api_access(self):
-        if self.__api_access is None:
-            deployed_app = self.get_deployed_app()
-            self.__api_access = ApiAccessService(
+    @classmethod
+    def __get_api_access(cls):
+        if cls.api_access is None:
+            deployed_app = cls.get_deployed_app(cls)
+            cls.api_access = ApiAccessService(
                 xsuaa_service_url=deployed_app.xsuaa_service_url,
                 xsuaa_api_url=deployed_app.xsuaa_api_url)
-        return self.__api_access
+        return cls.api_access
 
-    def get_ias_access(self, ias_name):
-        if self.__ias_access is None:
-            self.__ias_access = IasAccess(ias_name=ias_name)
-        return self.__ias_access
+    @classmethod
+    def get_ias_access(cls, ias_name):
+        if cls.ias_access is None:
+            cls.ias_access = IasAccess(ias_name=ias_name)
+        return cls.ias_access
 
     def __perform_get_request(self, path, access_token=None, additional_headers={}):
         url = 'https://{}-{}.{}{}'.format(
@@ -213,7 +231,8 @@ class TestTokenClient(SampleTest):
 
     def get_app(self):
         logging.info(RUN_TESTS.format("TokenClientUsage"))
-        return CFApp(name='java-tokenclient-usage', xsuaa_service_name='xsuaa-token-client')
+        self.cf_app = CFApp(name='java-tokenclient-usage', xsuaa_service_name='xsuaa-token-client')
+        return self.cf_app
 
     def test_hello_token_client(self):
         logging.info(RUN_TEST.format("TestTokenClient.test_hello_token_client"))
@@ -230,7 +249,8 @@ class TestJavaSecurity(SampleTest):
 
     def get_app(self):
         logging.info(RUN_TESTS.format("JavaSecurityUsage"))
-        return CFApp(name='java-security-usage', xsuaa_service_name='xsuaa-java-security')
+        self.cf_app = CFApp(name='java-security-usage', xsuaa_service_name='xsuaa-java-security')
+        return self.cf_app
 
     def test_hello_java_security(self):
         logging.info(RUN_TEST.format("TestJavaSecurity.test_hello_java_security"))
@@ -262,7 +282,9 @@ class TestJavaSecurity(SampleTest):
 class TestSpringSecurityHybrid(SampleTest):
 
     def get_app(self):
-        return CFApp(name='spring-security-hybrid-usage', xsuaa_service_name='xsuaa-authn', identity_service_name='ias-authn')
+        logging.info(RUN_TESTS.format("SpringSecurityHybrid"))
+        self.cf_app = CFApp(name='spring-security-hybrid-usage', xsuaa_service_name='xsuaa-authn', identity_service_name='ias-authn')
+        return self.cf_app
 
     def test_sayHello_xsuaa(self):
         resp = self.perform_get_request('/sayHello')
@@ -289,7 +311,9 @@ class TestSpringSecurityHybrid(SampleTest):
 class TestJavaSecurityIas(SampleTest):
 
     def get_app(self):
-        return CFApp(name='java-security-usage-ias', identity_service_name='ias-java-security')
+        logging.info(RUN_TESTS.format("JavaSecurityIas"))
+        self.cf_app = CFApp(name='java-security-usage-ias', identity_service_name='ias-java-security')
+        return self.cf_app
 
     def test_sayHello_ias(self):
         resp = self.perform_get_request('/hello-java-security-ias')
@@ -304,9 +328,10 @@ class TestJavaSecurityIas(SampleTest):
 class TestSpringSecurity(SampleTest):
 
     def get_app(self):
-        logging.info(RUN_TESTS.format("SpringSecurityUsage"))
-        return CFApp(name='spring-security-xsuaa-usage', xsuaa_service_name='xsuaa-authentication',
-                     app_router_name='approuter-spring-security-xsuaa-usage')
+        logging.info(RUN_TESTS.format("SpringSecurityUsageMtls"))
+        self.cf_app = CFApp(name='spring-security-xsuaa-usage', xsuaa_service_name='xsuaa-authentication',
+                            app_router_name='approuter-spring-security-xsuaa-usage')
+        return self.cf_app
 
     def test_sayHello(self):
         logging.info(RUN_TEST.format("TestSpringSecurity.test_sayHello'"))
@@ -362,14 +387,39 @@ class TestSpringSecurity(SampleTest):
         self.assertEqual(resp.status, 200, EXPECT_200)
 
 
+class TestSpringSecurityNonMtls(SampleTest):
+
+    def get_app(self):
+        logging.info(RUN_TESTS.format("SpringSecurityUsageNonMtls"))
+        self.cf_app = CFApp(name='spring-security-xsuaa-usage',
+                            xsuaa_service_name='xsuaa-authentication',
+                            app_router_name='approuter-spring-security-xsuaa-usage',
+                            security_descriptor='xs-security-deprecated.json')
+        return self.cf_app
+
+    def test_tokenFlows(self):
+        logging.info(RUN_TEST.format("TestSpringSecurity.test_tokenFlows"))
+        self.add_user_to_role('Viewer')
+        resp = self.perform_get_request_with_token('/v2/sayHello')
+        self.assertEqual(resp.status, 200, EXPECT_200)
+        resp = self.perform_get_request_with_token('/v3/requestClientCredentialsToken')
+        self.assertEqual(resp.status, 200, EXPECT_200)
+        resp = self.perform_get_request_with_token('/v3/requestUserToken')
+        self.assertEqual(resp.status, 200, EXPECT_200)
+        token = self.get_token()
+        path_with_refresh_token = '/v3/requestRefreshToken/' + token.get('refresh_token')
+        resp = self.perform_get_request_with_token(path_with_refresh_token)
+        self.assertEqual(resp.status, 200, EXPECT_200)
+
 
 class TestJavaBuildpackApiUsage(SampleTest):
 
     def get_app(self):
         logging.info(RUN_TESTS.format("JavaBuildpackApiUsage"))
-        return CFApp(name='sap-java-buildpack-api-usage',
-                     xsuaa_service_name='xsuaa-buildpack',
-                     app_router_name='approuter-sap-java-buildpack-api-usage')
+        self.cf_app = CFApp(name='sap-java-buildpack-api-usage',
+                            xsuaa_service_name='xsuaa-buildpack',
+                            app_router_name='approuter-sap-java-buildpack-api-usage')
+        return self.cf_app
 
     def test_hello_token_servlet(self):
         logging.info(RUN_TEST.format("TestJavaBuildpackApiUsage.test_hello_token_servlet"))
@@ -389,7 +439,8 @@ class SpringSecurityBasicAuthTest(SampleTest):
 
     def get_app(self):
         logging.info(RUN_TESTS.format("SpringSecurityBasicAuthTest"))
-        return CFApp(name='spring-security-basic-auth', xsuaa_service_name='xsuaa-basic')
+        self.cf_app = CFApp(name='spring-security-basic-auth', xsuaa_service_name='xsuaa-basic')
+        return self.cf_app
 
     def test_hello_token(self):
         logging.info(RUN_TEST.format("SpringSecurityBasicAuthTest.test_hello_token"))
@@ -401,7 +452,8 @@ class SpringSecurityBasicAuthTest(SampleTest):
         self.assertEqual(resp.status, 403, EXPECT_403)
 
     def test_hello_token_status_ok(self):
-        # second test needed because tokens are cached in application
+        # app restart needed because tokens are cached in application
+        self.cf_app.restart()
         logging.info(RUN_TEST.format("SpringSecurityBasicAuthTest.test_hello_token_status_ok"))
         self.add_user_to_role('BASIC_AUTH_API_Viewer')
         resp = self.perform_get_request('/hello-token', username=self.credentials.username,
@@ -414,9 +466,10 @@ class SpringWebfluxSecurityXsuaaUsage(SampleTest):
 
     def get_app(self):
         logging.info(RUN_TESTS.format("SpringWebfluxSecurityXsuaaUsage"))
-        return CFApp(name='spring-webflux-security-xsuaa-usage',
+        self.cf_app = CFApp(name='spring-webflux-security-xsuaa-usage',
                      xsuaa_service_name='xsuaa-webflux',
                      app_router_name='approuter-spring-webflux-security-xsuaa-usage')
+        return self.cf_app
 
     def test_say_hello(self):
         logging.info(RUN_TEST.format("SpringWebfluxSecurityXsuaaUsage.test_say_hello"))
@@ -469,6 +522,40 @@ class HttpUtil:
         self.__add_headers(req, access_token, additional_headers)
         return self.__execute(req)
 
+    def post_request_x509(self, url, data=None, access_token=None, certificate=None, key=None):
+        with open("cert.pem", "w") as cert_pem:
+            cert_pem.write(certificate)
+        with open("key.pem", "w") as key_pem:
+            key_pem.write(key)
+        host = url = url.replace("https://", "")
+        url_path = '/oauth/token'
+
+        context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)  # PROTOCOL_SSLV3 doesn't work
+        context.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
+
+        connection = http.client.HTTPSConnection(host, port=443, context=context, timeout=500)
+        connection.set_debuglevel(1)
+        connection.request(method="POST", url=url_path, body=data, headers={'Content-Type': 'application/x-www-form-urlencoded', 'Connection': 'close'})
+
+        logging.debug('Performing POST request over mTLS to {} {}'
+                      .format(url, 'with access token: ' + access_token if access_token else 'without access token'))
+        response = connection.getresponse()
+        body_decoded = response.read().decode()
+        logging.debug('Response from POST request over mTLS: status {} - data {}'.format(response.status, body_decoded))
+        connection.close()
+        if response.status == 200:
+            return json.loads(body_decoded)
+        else:
+            logging.error('mTLS post request failed - {}'.format(response.status))
+            return None
+
+    def encode_request_body(self, clientid, grant_type, username=None, password=None):
+        return urlencode({'client_id': clientid,
+                                   'grant_type': grant_type,
+                                   'response_type': 'token',
+                                   'username': username,
+                                   'password': password}).encode()
+
     def get_token(self, xsuaa_service_url, clientid, clientsecret, grant_type, username=None, password=None):
         post_req_body = urlencode({'client_id': clientid,
                                    'client_secret': clientsecret,
@@ -785,6 +872,10 @@ class DeployedApp:
     def xsuaa_service_url(self):
         return self.get_credentials_property('url')
 
+    @property
+    def xsuaa_cert_url(self):
+        return self.get_credentials_property('certurl')
+
     @property
     def clientid(self):
         return self.get_credentials_property('clientid')
@@ -793,6 +884,18 @@ class DeployedApp:
     def clientsecret(self):
         return self.get_credentials_property('clientsecret')
 
+    @property
+    def certificate(self):
+        return self.get_credentials_property('certificate')
+
+    @property
+    def key(self):
+        return self.get_credentials_property('key')
+
+    @property
+    def credential_type(self):
+        return self.get_credentials_property('credential-type')
+
     @property
     def ias_service_url(self):
         return self.get_ias_credentials_property('url')
@@ -816,7 +919,7 @@ class DeployedApp:
 
 
 class CFApp:
-    def __init__(self, name, xsuaa_service_name=None, app_router_name=None, identity_service_name=None):
+    def __init__(self, name, xsuaa_service_name=None, app_router_name=None, identity_service_name=None, security_descriptor=None):
         if name is None:
             raise (Exception('Name must be provided'))
         self.name = name
@@ -824,16 +927,25 @@ class CFApp:
         self.app_router_name = app_router_name
         self.identity_service_name = identity_service_name
         self.ias_access = None
+        if security_descriptor is None:
+            self.security_descriptor = 'xs-security.json'
+        else:
+            self.security_descriptor = security_descriptor
 
     @property
     def working_dir(self):
         return './' + self.name
 
+    def restart(self):
+        subprocess.run(
+            ['cf', 'restart', self.name],
+            cwd=self.working_dir, stdout=cf_logs, check=True)
+
     def deploy(self):
         if self.xsuaa_service_name is not None:
             logging.info("Creating Xsuaa service '{}'".format(self.xsuaa_service_name))
             subprocess.run(
-                ['cf', 'create-service', 'xsuaa', 'application', self.xsuaa_service_name, '-c', 'xs-security.json'],
+                ['cf', 'create-service', 'xsuaa', 'application', self.xsuaa_service_name, '-c', self.security_descriptor],
                 cwd=self.working_dir, stdout=cf_logs, check=True)
         if self.identity_service_name is not None and self.ias_access is None:
             self.ias_access = IasAccess(self.identity_service_name)
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java
index 65993597..73965b2b 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java
@@ -12,9 +12,9 @@ import static org.assertj.core.util.Lists.newArrayList;
 
 public class HttpHeadersTest {
 
-	private HttpHeader header = new HttpHeader("a", "a_value");
-	private HttpHeader anotherHeader = new HttpHeader("b", "b_value");
-	private HttpHeader sameKeyDifferentValue = new HttpHeader("a", "different_value");
+	private final HttpHeader header = new HttpHeader("a", "a_value");
+	private final HttpHeader anotherHeader = new HttpHeader("b", "b_value");
+	private final HttpHeader sameKeyDifferentValue = new HttpHeader("a", "different_value");
 
 	@Test
 	public void equals_sameHeaders_isEqual() {
@@ -38,9 +38,13 @@ public class HttpHeadersTest {
 		HttpHeaders headers2 = new HttpHeaders(anotherHeader);
 		HttpHeaders headers3 = new HttpHeaders(sameKeyDifferentValue);
 
-		assertThat(headers1).isNotEqualTo(headers2);
-		assertThat(headers1).isNotEqualTo(headers3);
+		assertThat(headers1).isNotEqualTo(headers2).isNotEqualTo(headers3);
 		assertThat(headers2).isNotEqualTo(headers3);
 	}
 
+	@Test
+	public void toStringTest() {
+		HttpHeaders headers = new HttpHeaders(header, anotherHeader);
+		assertThat(headers).hasToString("HttpHeaders: [ \"a: a_value\", \"b: b_value\" ]");
+	}
 }
\ No newline at end of file