Repository: SAP/cloud-security-xsuaa-integration
Pull request #208 (2020-01-16 14:59:30)

Migration
Created migration guide and simplified migration in lib.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6351fc9b..27f01bed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,6 @@
 
 All notable changes to this project will be documented in this file.
 
-## 2.4.1-SNAPSHOT
-- [java-security] Migration Guide and some little improvements / enhancements.
-
 ## 2.4.0-SNAPSHOT
 - [java-security] Initial / Snapshot version of the new plain Java security libraries as documented [here](/README.md#token-validation-for-java-applications).
 - [token-client] [OidcConfigurationServiceWithCache](/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OidcConfigurationServiceWithCache.java) supports basically Open-id Configuration endpoints as documented [here](https://docs.cloudfoundry.org/api/uaa/version/74.4.0/index.html#openid-connect-flow).
diff --git a/api/pom.xml b/api/pom.xml
index c9121b1f..652d12ae 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -4,12 +4,12 @@
 
 	<groupId>com.sap.cloud.security.xsuaa</groupId>
 	<artifactId>api</artifactId>
-	<version>2.4.1-SNAPSHOT</version>
+	<version>2.4.0-SNAPSHOT</version>
 
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<packaging>jar</packaging>
diff --git a/java-security/Migration_SpringSecurityProjects.md b/java-security/Migration_SpringSecurityProjects.md
new file mode 100644
index 00000000..60c740f4
--- /dev/null
+++ b/java-security/Migration_SpringSecurityProjects.md
@@ -0,0 +1,147 @@
+
+# Dependencies
+
+## Maven
+To use the new [java client lib](https://github.com/SAP/cloud-security-xsuaa-integration/) the dependencies declared in maven `pom.xml` need to be updated.
+
+First make sure you have the following dependencies defined in your pom.xml:
+
+```xml
+<dependency>
+  <groupId>org.springframework.security.oauth</groupId>
+  <artifactId>spring-security-oauth2</artifactId>
+  <version>2.4.0.RELEASE</version>
+</dependency>
+<dependency>
+  <groupId>org.springframework</groupId>
+  <artifactId>spring-aop</artifactId>
+  <version>4.3.25.RELEASE</version>
+</dependency>
+<dependency>
+  <groupId>com.sap.cloud.security.xsuaa</groupId>
+  <artifactId>java-security</artifactId>
+  <version>2.4.0-SNAPSHOT</version>
+</dependency>
+<dependency>
+  <groupId>com.sap.cloud.security.xsuaa</groupId>
+  <artifactId>java-security-test</artifactId>
+  <version>2.4.0-SNAPSHOT</version>
+  <scope>test</scope>
+</dependency>
+```
+
+Also ensure that you do not use outdated versions of maven plugins that might affect your build. 
+So for example if you use `findbugs-maven-plugin` or `jacoco-maven-plugin`, update them to a current version.
+
+Now you are ready to **remove** the old client library by deleting the following lines from the pom.xml:
+```xml
+<dependency>
+  <groupId>com.sap.xs2.security</groupId>
+  <artifactId>java-container-security</artifactId>
+</dependency>
+```
+<!-- Also remove the org.springframework.amqp:spring-rabbit dependency -->
+
+## Update App router
+The  from the app router needs to be updated as well. Find the `package.json` file inside 
+`src/main/approuter` directory and replace the content with the following:
+```json
+{
+  "name": "approuter",
+  "dependencies": {
+     "@sap/approuter": "6.1.0"
+   },
+   "scripts": {
+     "start": "node node_modules/@sap/approuter/approuter.js"
+    }
+}
+```
+
+## Update build packs
+Find the `manifest.yml` in the root directory and make sure to use up to date build pack versions.
+For example update `java-buildpack` to version `v4.27` and the nodejs buildpack to version `v1.6.49`.
+
+# Code changes
+After the dependencies have been changed, the project code probably needs some adjustments as well.
+
+If your security configuration was using the `SAPOfflineTokenServicesCloud` class from the java-container-security library,
+you need to change it slightly to use the `SAPOfflineTokenServicesCloud` adapter class from the new library.  
+
+For example see the following snippet on how to instantiate the `SAPOfflineTokenServicesCloud`. 
+
+```java
+@Bean
+@Profile("cloud")
+protected SAPOfflineTokenServicesCloud offlineTokenServices() {
+	return new SAPOfflineTokenServicesCloud(Environments.getCurrent().getXsuaaConfiguration());
+}
+```
+You might need to fix your java imports to get rid of the old import for the `SAPOfflineTokenServicesCloud` class.
+
+
+## Repair tests
+The `src/test/java/com/sap/bulletinboard/ads/testutils/JwtGenerator.java` class must be removed because the new java
+client test library provides it's own `JwtGenerator`.
+
+### Security configuration for tests
+If you want to overwrite the service configuration of the `SAPOfflineTokenServicesCloud` for your test, you can do so by
+using some test constants provided by the test library. The following snippet shows how to do that:
+```java 
+@Configuration
+public class TestSecurityConfig {
+	@Bean
+	@Primary
+	public SAPOfflineTokenServicesCloud sapOfflineTokenServices() {
+		OAuth2ServiceConfiguration configuration = OAuth2ServiceConfigurationBuilder
+				.forService(Service.XSUAA)
+				.withClientId(SecurityTestRule.DEFAULT_CLIENT_ID)
+				.withProperty(CFConstants.XSUAA.APP_ID, SecurityTestRule.DEFAULT_APP_ID)
+				.withProperty(CFConstants.XSUAA.UAA_DOMAIN, SecurityTestRule.DEFAULT_DOMAIN) //TODO
+				.build();
+		return new SAPOfflineTokenServicesCloud(configuration);
+	}
+}
+```
+
+### Unit testing 
+In your unit test you might want to generate jwt tokens and have them validated. This can be done with the new 
+`SecurityTestRule`. See the following snippet as example: 
+
+```java
+@ClassRule
+public static SecurityTestRule securityTestRule =
+	SecurityTestRule.getInstance(Service.XSUAA)
+		.setKeys("src/test/resources/publicKey.txt", "src/test/resources/privateKey.txt");
+```
+
+Using the SecurityTestRule you can use a preconfigured jwt generator to create JWT tokens with custom scopes for your tests.
+
+```java
+String jwt = securityTestRule.getPreconfiguredJwtGenerator()
+    .withScopes(WebSecurityConfig.DISPLAY_SCOPE, WebSecurityConfig.UPDATE_SCOPE)
+    .createToken()
+    .getBearerAccessToken();
+
+```
+
+<!-- 
+Remove the following files:
+CloudRabbitConfig.java
+StatisticsListener.java
+MockRabbitConfig.java
+StatisticsServiceClient.java
+
+In `SpringBootActuatorConfig` remove the `RabbitAutoConfiguration.class` autoconfiguration.
+
+In `AdvertisementController` remove the 
+`StatisticsServiceClient` injection, the field and all the field usages.
+-->
+
+# Enable local testing
+For local testing you might need to provide custom `VCAP_SERVICES` before you run the application. 
+The new security library requires the key value pair `"uaadomain" : "localhost"` in the `VCAP_SERVICES`
+under `xsuaa/credentials` to be able to validate the XSUAA tokens.
+
+# Things to check after migration 
+When your code compiles again you should first check that all your unit tests are running again. If you can test your
+application locally make sure that it is still working and finally test the application in cloud foundry.
diff --git a/java-security/README.md b/java-security/README.md
index 75c02355..90661ec6 100644
--- a/java-security/README.md
+++ b/java-security/README.md
@@ -41,33 +41,15 @@ A Java implementation of JSON Web Token (JWT) - [RFC 7519](https://tools.ietf.or
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>java-security</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 <dependency>
     <groupId>org.apache.httpcomponents</groupId>
     <artifactId>httpclient</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 ```
 
-You may need to add the following profile to allow snapshot versions:
-```xml
-<profiles>
-    <profile>
-        <id>allow-snapshots</id>
-        <activation><activeByDefault>true</activeByDefault></activation>
-        <repositories>
-            <repository>
-                <id>snapshots-repo</id>
-                <url>https://oss.sonatype.org/content/repositories/snapshots</url>
-                <releases><enabled>false</enabled></releases>
-                <snapshots><enabled>true</enabled></snapshots>
-            </repository>
-        </repositories>
-    </profile>
-</profiles>
-```
-
 ## Basic Usage
 
 ### Setup Step 1: Load the Service Configuration(s)
diff --git a/java-security/pom.xml b/java-security/pom.xml
index f0ee75bb..45dac06a 100644
--- a/java-security/pom.xml
+++ b/java-security/pom.xml
@@ -7,7 +7,7 @@
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>java-security</artifactId>
@@ -48,6 +48,18 @@
 		</dependency>
 
 
+		<!-- used for the spring adapter used by migration projects -->
+		<dependency>
+			<groupId>org.springframework.security.oauth</groupId>
+			<artifactId>spring-security-oauth2</artifactId>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework</groupId>
+			<artifactId>spring-beans</artifactId>
+			<scope>provided</scope>
+		</dependency>
+
 		<!-- test utilities -->
 		<dependency>
 			<groupId>junit</groupId>
diff --git a/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvParser.java b/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvParser.java
index acc372e0..abd27411 100644
--- a/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvParser.java
+++ b/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvParser.java
@@ -5,7 +5,6 @@ import com.sap.cloud.security.config.OAuth2ServiceConfigurationBuilder;
 import com.sap.cloud.security.config.Service;
 import com.sap.cloud.security.json.DefaultJsonObject;
 import com.sap.cloud.security.json.JsonObject;
-import com.sap.cloud.security.json.JsonParsingException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,20 +51,13 @@ class CFEnvParser {
 
 	public static OAuth2ServiceConfiguration extract(Service service, JsonObject serviceJsonObject) {
 		Map<String, String> serviceBindingProperties = serviceJsonObject.getKeyValueMap();
-		try {
-			Map<String, String> serviceBindingCredentials = serviceJsonObject.getJsonObject(CFConstants.CREDENTIALS)
-					.getKeyValueMap();
+		Map<String, String> serviceBindingCredentials = serviceJsonObject.getJsonObject(CFConstants.CREDENTIALS)
+				.getKeyValueMap();
 
-			return OAuth2ServiceConfigurationBuilder.forService(service)
-					.withProperties(serviceBindingCredentials)
-					.withProperty(SERVICE_PLAN, serviceBindingProperties.get(SERVICE_PLAN))
-					.build();
-		} catch (JsonParsingException e) {
-			String errDescription = "The credentials of 'VCAP_SERVICES' can not be parsed for service '"
-					+ service + "' ('" + e.getMessage() + "'). Please check the service binding.";
-			logger.error(errDescription);
-			throw new JsonParsingException(errDescription);
-		}
+		return OAuth2ServiceConfigurationBuilder.forService(service)
+				.withProperties(serviceBindingCredentials)
+				.withProperty(SERVICE_PLAN, serviceBindingProperties.get(SERVICE_PLAN))
+				.build();
 	}
 
 }
diff --git a/java-security/src/main/java/com/sap/cloud/security/spring/adapter/SAPOfflineTokenServicesCloud.java b/java-security/src/main/java/com/sap/cloud/security/spring/adapter/SAPOfflineTokenServicesCloud.java
new file mode 100644
index 00000000..794c3623
--- /dev/null
+++ b/java-security/src/main/java/com/sap/cloud/security/spring/adapter/SAPOfflineTokenServicesCloud.java
@@ -0,0 +1,59 @@
+package com.sap.cloud.security.spring.adapter;
+
+import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.XsuaaToken;
+import com.sap.cloud.security.token.validation.CombiningValidator;
+import com.sap.cloud.security.token.validation.Validator;
+import com.sap.cloud.security.token.validation.validators.JwtValidatorBuilder;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
+import org.springframework.security.oauth2.provider.AuthorizationRequest;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices, InitializingBean {
+
+	private final Supplier<Validator<Token>> validatorSupplier;
+	private final OAuth2ServiceConfiguration serviceConfiguration;
+	private Validator<Token> tokenValidator;
+
+	public SAPOfflineTokenServicesCloud(OAuth2ServiceConfiguration serviceConfiguration) {
+		this(serviceConfiguration, () -> JwtValidatorBuilder.getInstance(serviceConfiguration).build());
+	}
+
+	SAPOfflineTokenServicesCloud(OAuth2ServiceConfiguration serviceConfiguration, Supplier<Validator<Token>> validatorSupplier) {
+		this.serviceConfiguration = serviceConfiguration;
+		this.validatorSupplier = validatorSupplier;
+	}
+
+	@Override
+	public OAuth2Authentication loadAuthentication(String accessToken)
+			throws AuthenticationException, InvalidTokenException {
+		XsuaaToken token = new XsuaaToken(accessToken);
+		Set<String> scopes = token.getScopes().stream().collect(Collectors.toSet());
+
+		AuthorizationRequest authorizationRequest = new AuthorizationRequest(new HashMap<>(), null,
+				serviceConfiguration.getClientId(), scopes, new HashSet<>(), null,
+				tokenValidator.validate(token).isValid(), "", "", null);
+
+		return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), null);
+	}
+
+	@Override
+	public void afterPropertiesSet() {
+		tokenValidator = validatorSupplier.get();
+	}
+
+	@Override public OAuth2AccessToken readAccessToken(String accessToken) {
+		throw new UnsupportedOperationException("Not supported: read access token");
+	}
+}
\ No newline at end of file
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/TokenClaims.java b/java-security/src/main/java/com/sap/cloud/security/token/TokenClaims.java
index 308cbcce..211c96a9 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/TokenClaims.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/TokenClaims.java
@@ -24,7 +24,8 @@ public final class TokenClaims {
 		public static final String EMAIL = "email";
 		public static final String ORIGIN = "origin";
 		public static final String GRANT_TYPE = "grant_type"; // OAuth grant type used for token creation
-		public static final String SUBACCOUNT_ID = "zid"; // tenant GUID, identity zone id
+		public static final String SUBDOMAIN = "zdn"; // subdomain of the tenant the JWT belongs to
+		public static final String SUBACCOUNT_ID = "zid"; // tenant GUID
 		public static final String CLIENT_ID = "cid"; // OAuth client identifier
 		public static final String SCOPES = "scope"; // list of scopes including application id, e.g.
 														// "my-app!t123.Display"
diff --git a/pom.xml b/pom.xml
index 07917336..56ec5127 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
 
 	<groupId>com.sap.cloud.security.xsuaa</groupId>
 	<artifactId>parent</artifactId>
-	<version>2.4.1-SNAPSHOT</version>
+	<version>2.4.0-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>parent</name>
diff --git a/samples/java-security-usage/pom.xml b/samples/java-security-usage/pom.xml
index d1ed2dff..257caec1 100755
--- a/samples/java-security-usage/pom.xml
+++ b/samples/java-security-usage/pom.xml
@@ -3,7 +3,7 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.sap.cloud.security.xssec.samples</groupId>
     <artifactId>java-security-usage</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <profiles>
@@ -24,7 +24,7 @@
     <properties>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
-        <sap.security.client.version>2.4.1-SNAPSHOT</sap.security.client.version>
+        <sap.security.client.version>2.4.0-SNAPSHOT</sap.security.client.version>
         <apache.httpclient.version>4.5.8</apache.httpclient.version>
         <javax.servlet.api.version>3.1.0</javax.servlet.api.version>
         <wiremock.version>2.25.1</wiremock.version>
diff --git a/samples/java-tokenclient-usage/pom.xml b/samples/java-tokenclient-usage/pom.xml
index 3d9f7256..a7cbac2b 100755
--- a/samples/java-tokenclient-usage/pom.xml
+++ b/samples/java-tokenclient-usage/pom.xml
@@ -3,11 +3,11 @@
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.sap.cloud.security.xssec.samples</groupId>
     <artifactId>java-tokenclient-usage</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
     <packaging>war</packaging>
 
     <properties>
-        <xsuaa.client.version>2.4.1-SNAPSHOT</xsuaa.client.version>
+        <xsuaa.client.version>2.4.0-SNAPSHOT</xsuaa.client.version>
         <apache.httpclient.version>4.5.8</apache.httpclient.version>
         <javax.servlet.api.version>3.0.1</javax.servlet.api.version>
     </properties>
diff --git a/samples/sap-java-buildpack-api-usage/pom.xml b/samples/sap-java-buildpack-api-usage/pom.xml
index de2ff8cf..cf91b199 100755
--- a/samples/sap-java-buildpack-api-usage/pom.xml
+++ b/samples/sap-java-buildpack-api-usage/pom.xml
@@ -3,7 +3,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>com.sap.cloud.security.xssec.samples</groupId>
 	<artifactId>sap-java-buildpack-api-usage</artifactId>
-	<version>2.4.1-SNAPSHOT</version>
+	<version>2.4.0-SNAPSHOT</version>
 	<packaging>war</packaging>
 	<build>
 		<plugins>
@@ -21,7 +21,7 @@
 		<dependency>
 			<groupId>com.sap.cloud.security.xsuaa</groupId>
 			<artifactId>api</artifactId>
-			<version>2.4.1-SNAPSHOT</version>
+			<version>2.4.0-SNAPSHOT</version>
 			<scope>provided</scope>
 		</dependency>
 		<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
diff --git a/samples/spring-security-basic-auth/pom.xml b/samples/spring-security-basic-auth/pom.xml
index b3417f4a..bd8ea002 100644
--- a/samples/spring-security-basic-auth/pom.xml
+++ b/samples/spring-security-basic-auth/pom.xml
@@ -11,7 +11,7 @@
 	</parent>
 
 	<artifactId>spring-security-basic-auth</artifactId>
-	<version>2.4.1-SNAPSHOT</version>
+	<version>2.4.0-SNAPSHOT</version>
 	<name>spring-security-basic-auth</name>
 
 	<properties>
diff --git a/samples/spring-security-xsuaa-usage/pom.xml b/samples/spring-security-xsuaa-usage/pom.xml
index fed10def..63434f34 100644
--- a/samples/spring-security-xsuaa-usage/pom.xml
+++ b/samples/spring-security-xsuaa-usage/pom.xml
@@ -14,7 +14,7 @@
 
 	<groupId>com.sap.cloud.security.samples</groupId>
 	<artifactId>spring-security-xsuaa-usage</artifactId>
-	<version>2.4.1-SNAPSHOT</version>
+	<version>2.4.0-SNAPSHOT</version>
 	<name>spring-security-xsuaa-usage</name>
 
 	<properties>
diff --git a/samples/spring-webflux-security-xsuaa-usage/pom.xml b/samples/spring-webflux-security-xsuaa-usage/pom.xml
index a7c21f8c..fd5596db 100644
--- a/samples/spring-webflux-security-xsuaa-usage/pom.xml
+++ b/samples/spring-webflux-security-xsuaa-usage/pom.xml
@@ -12,7 +12,7 @@
 
     <groupId>com.sap.cloud.security.samples</groupId>
     <artifactId>spring-webflux-security-xsuaa-usage</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
     <name>spring-webflux-security-xsuaa-usage</name>
 
     <properties>
diff --git a/spring-xsuaa-it/pom.xml b/spring-xsuaa-it/pom.xml
index 29815bc5..c2b6c77c 100644
--- a/spring-xsuaa-it/pom.xml
+++ b/spring-xsuaa-it/pom.xml
@@ -11,7 +11,7 @@
 	</parent>
 
 	<artifactId>spring-xsuaa-it</artifactId>
-	<version>2.4.1-SNAPSHOT</version>
+	<version>2.4.0-SNAPSHOT</version>
 	<name>spring-xsuaa-it</name>
 
 	<properties>
diff --git a/spring-xsuaa-mock/README.md b/spring-xsuaa-mock/README.md
index f96918f2..d7439b8d 100644
--- a/spring-xsuaa-mock/README.md
+++ b/spring-xsuaa-mock/README.md
@@ -17,7 +17,7 @@ The default implementation offers already valid *token_keys* for JWT tokens, tha
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>spring-xsuaa-mock</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 <dependency> <!-- new with version 1.5.0 - provided with org.springframework.boot:spring-boot-starter:jar -->
     <groupId>org.springframework.boot</groupId>
diff --git a/spring-xsuaa-mock/pom.xml b/spring-xsuaa-mock/pom.xml
index a3324c6b..39829050 100644
--- a/spring-xsuaa-mock/pom.xml
+++ b/spring-xsuaa-mock/pom.xml
@@ -7,7 +7,7 @@
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>spring-xsuaa-mock</artifactId>
diff --git a/spring-xsuaa-starter/pom.xml b/spring-xsuaa-starter/pom.xml
index b679de76..d4e96135 100644
--- a/spring-xsuaa-starter/pom.xml
+++ b/spring-xsuaa-starter/pom.xml
@@ -14,11 +14,11 @@
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>xsuaa-spring-boot-starter</artifactId>
-	<version>2.4.1-SNAPSHOT</version>
+	<version>2.4.0-SNAPSHOT</version>
 	<name>SAP Spring Boot Xsuaa Starter</name>
 	<description>SAP Starter for integrating application with XSUAA service</description>
 	<url>https://github.com/SAP/cloud-security-xsuaa-integration</url>
diff --git a/spring-xsuaa/README.md b/spring-xsuaa/README.md
index a5c2f719..96866903 100644
--- a/spring-xsuaa/README.md
+++ b/spring-xsuaa/README.md
@@ -23,7 +23,7 @@ This library enhances the [spring-security](https://github.com/spring-projects/s
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>spring-xsuaa</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 <dependency> <!-- new with version 1.5.0 -->
     <groupId>org.apache.logging.log4j</groupId>
@@ -38,7 +38,7 @@ This library enhances the [spring-security](https://github.com/spring-projects/s
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>xsuaa-spring-boot-starter</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 ```
 
diff --git a/spring-xsuaa/pom.xml b/spring-xsuaa/pom.xml
index 3df7f664..d06e20c0 100644
--- a/spring-xsuaa/pom.xml
+++ b/spring-xsuaa/pom.xml
@@ -7,7 +7,7 @@
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>spring-xsuaa</artifactId>
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
index b77ceac8..20bf0ae7 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
@@ -1,25 +1,28 @@
 package com.sap.cloud.security.xsuaa.token.authentication;
 
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+
 import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtValidators;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
-import org.springframework.web.client.RestOperations;
 
-import java.util.*;
-import java.util.stream.Collectors;
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import org.springframework.web.client.RestOperations;
 
 public class XsuaaJwtDecoderBuilder {
 
+	private XsuaaServiceConfiguration configuration;
 	int decoderCacheValidity; // in seconds
 	int decoderCacheSize;
+	OAuth2TokenValidator<Jwt> xsuaaTokenValidators;
+	OAuth2TokenValidator<Jwt> defaultTokenValidators;
 	Collection<PostValidationAction> postValidationActions;
-	private XsuaaServiceConfiguration configuration;
 	private RestOperations restOperations;
-	private List<OAuth2TokenValidator<Jwt>> xsuaaTokenValidators;
 
 	/**
 	 * Utility for building a JWT decoder configuration
@@ -29,8 +32,8 @@ public class XsuaaJwtDecoderBuilder {
 	 */
 	public XsuaaJwtDecoderBuilder(XsuaaServiceConfiguration configuration) {
 		this.configuration = configuration;
-		xsuaaTokenValidators = new ArrayList<>();
-		xsuaaTokenValidators.add(new XsuaaAudienceValidator(configuration));
+		withDefaultValidators(JwtValidators.createDefault());
+		withTokenValidators(new XsuaaAudienceValidator(configuration));
 		withDecoderCacheSize(100);
 		withDecoderCacheTime(900);
 	}
@@ -41,8 +44,11 @@ public class XsuaaJwtDecoderBuilder {
 	 * @return JwtDecoder
 	 */
 	public JwtDecoder build() {
+		DelegatingOAuth2TokenValidator<Jwt> combinedTokenValidators = new DelegatingOAuth2TokenValidator<>(
+				defaultTokenValidators,
+				xsuaaTokenValidators);
 		XsuaaJwtDecoder jwtDecoder = new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize,
-				getValidators(), postValidationActions);
+				combinedTokenValidators, postValidationActions);
 		Optional.ofNullable(restOperations).ifPresent(jwtDecoder::setRestOperations);
 		return jwtDecoder;
 	}
@@ -53,13 +59,11 @@ public class XsuaaJwtDecoderBuilder {
 	 * @return ReactiveJwtDecoder
 	 */
 	public ReactiveJwtDecoder buildAsReactive() {
-		return new ReactiveXsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize, getValidators(),
-				postValidationActions);
-	}
-
-	private DelegatingOAuth2TokenValidator<Jwt> getValidators() {
-		return new DelegatingOAuth2TokenValidator<>(new DelegatingOAuth2TokenValidator<>(xsuaaTokenValidators),
-				JwtValidators.createDefault());
+		DelegatingOAuth2TokenValidator<Jwt> combinedTokenValidators = new DelegatingOAuth2TokenValidator<>(
+				defaultTokenValidators,
+				xsuaaTokenValidators);
+		return new ReactiveXsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize,
+				combinedTokenValidators, postValidationActions);
 	}
 
 	/**
@@ -109,8 +113,11 @@ public class XsuaaJwtDecoderBuilder {
 	 *            the token validators
 	 * @return this
 	 */
+	// var arg it is only being assigned to a OAuth2TokenValidator<Jwt>[], therefore
+	// its type safe.
+	@SuppressWarnings("unchecked")
 	public XsuaaJwtDecoderBuilder withTokenValidators(OAuth2TokenValidator<Jwt>... tokenValidators) {
-		xsuaaTokenValidators = Arrays.asList(tokenValidators);
+		this.xsuaaTokenValidators = new DelegatingOAuth2TokenValidator<>(tokenValidators);
 		return this;
 	}
 
@@ -128,15 +135,8 @@ public class XsuaaJwtDecoderBuilder {
 		return this;
 	}
 
-	/**
-	 * Disables the JWT {@link XsuaaAudienceValidator} which is enabled by default.
-	 *
-	 * @return the builder itself.
-	 */
-	public XsuaaJwtDecoderBuilder withoutXsuaaAudienceValidator() {
-		xsuaaTokenValidators = xsuaaTokenValidators.stream()
-				.filter(validator -> !(validator instanceof XsuaaAudienceValidator))
-				.collect(Collectors.toList());
+	XsuaaJwtDecoderBuilder withDefaultValidators(OAuth2TokenValidator<Jwt>... defaultTokenValidators) {
+		this.defaultTokenValidators = new DelegatingOAuth2TokenValidator<>(defaultTokenValidators);
 		return this;
 	}
 
diff --git a/token-client/README.md b/token-client/README.md
index 9cd6d629..780ab167 100644
--- a/token-client/README.md
+++ b/token-client/README.md
@@ -23,7 +23,7 @@ The Resource owner password credentials (i.e., username and password) can be use
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>token-client</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 <dependency>
   <groupId>org.apache.httpcomponents</groupId>
@@ -57,7 +57,7 @@ The `DefaultOAuth2TokenService` can also be instantiated with a custom `Closeabl
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>token-client</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 ```
 
@@ -80,7 +80,7 @@ In context of a Spring Boot application you may like to leverage auto-configurat
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>xsuaa-spring-boot-starter</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
 </dependency>
 ```
 
diff --git a/token-client/pom.xml b/token-client/pom.xml
index 6ecf1136..0d1d5edd 100644
--- a/token-client/pom.xml
+++ b/token-client/pom.xml
@@ -7,7 +7,7 @@
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>token-client</artifactId>


Test patch:
diff --git a/java-security-test/README.md b/java-security-test/README.md
index d02ea157..6fdc9703 100644
--- a/java-security-test/README.md
+++ b/java-security-test/README.md
@@ -18,7 +18,7 @@ It includes for example a `JwtGenerator` that generates JSON Web Tokens (JWT) th
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>java-security-test</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
     <scope>test</scope>
 </dependency>
 ```
diff --git a/java-security-test/pom.xml b/java-security-test/pom.xml
index de4db758..f07390f5 100644
--- a/java-security-test/pom.xml
+++ b/java-security-test/pom.xml
@@ -7,7 +7,7 @@
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>java-security-test</artifactId>
@@ -15,7 +15,7 @@
 	<packaging>jar</packaging>
 
 	<properties>
-		<sap.security.client.version>2.4.1-SNAPSHOT</sap.security.client.version>
+		<sap.security.client.version>2.4.0-SNAPSHOT</sap.security.client.version>
 		<apache.httpclient.version>4.5.8</apache.httpclient.version>
 		<jetty.version>9.4.24.v20191120</jetty.version>
 		<javax.servlet-api.version>3.1.0</javax.servlet-api.version>
diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java
index ba608ad3..e6ab7805 100644
--- a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java
+++ b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTestRule.java
@@ -29,6 +29,8 @@ import javax.servlet.Filter;
 import javax.servlet.Servlet;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
 import java.util.*;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.*;
@@ -158,6 +160,27 @@ public class SecurityTestRule extends ExternalResource {
 		return this;
 	}
 
+	/**
+	 * Same as {@link SecurityTestRule#setKeys(RSAKeys)} except you can pass in the
+	 * path to the public and private keys and the {@link RSAKeys} object will be
+	 * built for you. Any checked exceptions are caught and rethrown as runtime
+	 * exceptions for test convenience.
+	 *
+	 * @param publicKeyPath
+	 *            path to public key file.
+	 * @param privateKeyPath
+	 *            path to private key file.
+	 * @return the rule itself.
+	 */
+	public SecurityTestRule setKeys(String publicKeyPath, String privateKeyPath) {
+		try {
+			this.keys = RSAKeys.fromKeyFiles(publicKeyPath, privateKeyPath);
+		} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
+			throw new RuntimeException(e);
+		}
+		return this;
+	}
+
 	/**
 	 * Overwrites the private/public key pair to be used. The private key is used to
 	 * sign the jwt token. The public key is provided by jwks endpoint (on behalf of
diff --git a/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java b/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java
index fb4cb5a0..7769cac4 100644
--- a/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java
+++ b/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java
@@ -87,14 +87,14 @@ public class JwtGeneratorTest {
 	@Test
 	public void withClaim_containsClaim() {
 		String clientId = "myClientId";
-		String email = "john.doe@mail.de";
+		String subdomain = "subdomain";
 
 		Token token = cut
-				.withClaimValue(TokenClaims.XSUAA.EMAIL, email)
+				.withClaimValue(TokenClaims.XSUAA.SUBDOMAIN, subdomain)
 				.withClaimValue(TokenClaims.XSUAA.CLIENT_ID, clientId)
 				.createToken();
 
-		assertThat(token.getClaimAsString(TokenClaims.XSUAA.EMAIL)).isEqualTo(email);
+		assertThat(token.getClaimAsString(TokenClaims.XSUAA.SUBDOMAIN)).isEqualTo(subdomain);
 		assertThat(token.getClaimAsString(TokenClaims.XSUAA.CLIENT_ID)).isEqualTo(clientId);
 	}
 
diff --git a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
index c1dd2a4d..7d4e98bb 100644
--- a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
+++ b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
@@ -24,10 +24,13 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
 import java.util.stream.Collectors;
 
 import static com.sap.cloud.security.config.Service.XSUAA;
-import static com.sap.cloud.security.test.ApplicationServerOptions.*;
+import static com.sap.cloud.security.test.ApplicationServerOptions.forService;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
@@ -48,14 +51,10 @@ public class SecurityTestRuleTest {
 			.addApplicationServlet(TestServlet.class, "/hi");
 
 	@Test
-	public void getTokenKeysRequest_responseContainsExpectedTokenKeys() throws IOException {
-		HttpGet httpGet = new HttpGet("http://localhost:" + PORT + "/token_keys");
+	public void getTokenKeysRequest_responseContainsExpectedTokenKeys()
+			throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
 
-		try (CloseableHttpResponse response = HttpClients.createDefault().execute(httpGet)) {
-			assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-			JsonWebKeySet keySet = JsonWebKeySetFactory.createFromJson(readContent(response));
-			assertThat(keySet.getKeyByAlgorithmAndId(JwtSignatureAlgorithm.RS256, "default-kid")).isNotNull();
-		}
+		assertThatMockKeyServiceServesPublicKey(RSA_KEYS.getPublic(), PORT);
 	}
 
 	@Test
@@ -88,12 +87,15 @@ public class SecurityTestRuleTest {
 		}
 	}
 
-	private String readContent(CloseableHttpResponse response) throws IOException {
-		return IOUtils.readLines(response.getEntity().getContent(), UTF_8).stream()
-				.collect(Collectors.joining());
+	@Test
+	public void setKeys_invalidPath_throwsException()
+			throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
+		assertThatThrownBy(() -> SecurityTestRule.getInstance(XSUAA)
+				.setKeys("doesNotExist", "doesNotExist"))
+						.isInstanceOf(RuntimeException.class);
 	}
 
-	public static class TestRuleWithMockServlet {
+	public static class SecurityTestRuleWithMockServlet {
 
 		private HttpServlet mockServlet = Mockito.mock(HttpServlet.class);
 
@@ -113,7 +115,7 @@ public class SecurityTestRuleTest {
 
 	}
 
-	public static class SecurityTestRuleTestWithoutApplicationServer {
+	public static class SecurityTestRuleWithoutApplicationServer {
 
 		@Rule
 		public SecurityTestRule rule = SecurityTestRule.getInstance(XSUAA);
@@ -126,7 +128,7 @@ public class SecurityTestRuleTest {
 	}
 
 	// TODO IAS
-	public static class SecurityIntegrationApplicationServerFaults {
+	public static class SecurityTestRuleApplicationServerFaults {
 
 		@Test
 		public void onlyXsuaaIsSupportedYet() {
@@ -137,6 +139,22 @@ public class SecurityTestRuleTest {
 
 	}
 
+	public static class SecurityTestRuleWithRSAKeysPath {
+
+		private static final String PUBLIC_KEY_PATH = "src/main/resources/publicKey.txt";
+
+		@Rule
+		public SecurityTestRule rule = SecurityTestRule.getInstance(XSUAA)
+				.setKeys(PUBLIC_KEY_PATH, "src/main/resources/privateKey.txt");
+
+		@Test
+		public void testRuleIsInitializedCorrectly()
+				throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
+			assertThatMockKeyServiceServesPublicKey(RSAKeys.loadPublicKey(PUBLIC_KEY_PATH),
+					rule.getWireMockRule().port());
+		}
+	}
+
 	public static class TestServlet extends HttpServlet {
 		@Override
 		protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
@@ -146,4 +164,22 @@ public class SecurityTestRuleTest {
 			response.getWriter().print("Hi!");
 		}
 	}
+
+	private static String readContent(CloseableHttpResponse response) throws IOException {
+		return IOUtils.readLines(response.getEntity().getContent(), UTF_8).stream()
+				.collect(Collectors.joining());
+	}
+
+	private static void assertThatMockKeyServiceServesPublicKey(PublicKey expectedPublicKey, int mockKeyServicePort)
+			throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
+		HttpGet httpGet = new HttpGet("http://localhost:" + mockKeyServicePort + "/token_keys");
+		try (CloseableHttpResponse response = HttpClients.createDefault().execute(httpGet)) {
+			assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+			JsonWebKeySet keySet = JsonWebKeySetFactory.createFromJson(readContent(response));
+			PublicKey actualPublicKey = keySet
+					.getKeyByAlgorithmAndId(JwtSignatureAlgorithm.RS256, "default-kid").getPublicKey();
+
+			assertThat(actualPublicKey).isEqualTo(expectedPublicKey);
+		}
+	}
 }
diff --git a/java-security/src/test/java/com/sap/cloud/security/config/cf/CFEnvironmentTest.java b/java-security/src/test/java/com/sap/cloud/security/config/cf/CFEnvironmentTest.java
index 5269f4d4..12050b4e 100644
--- a/java-security/src/test/java/com/sap/cloud/security/config/cf/CFEnvironmentTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/config/cf/CFEnvironmentTest.java
@@ -6,7 +6,6 @@ import com.sap.cloud.security.config.Service;
 import com.sap.cloud.security.json.DefaultJsonObject;
 import com.sap.cloud.security.json.JsonObject;
 
-import com.sap.cloud.security.json.JsonParsingException;
 import org.apache.commons.io.IOUtils;
 import org.junit.Before;
 import org.junit.Test;
@@ -18,7 +17,6 @@ import static com.sap.cloud.security.config.cf.CFConstants.*;
 import static com.sap.cloud.security.config.cf.CFConstants.SERVICE_PLAN;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 public class CFEnvironmentTest {
 
@@ -57,15 +55,6 @@ public class CFEnvironmentTest {
 		assertThat(credentialsMap.get(CLIENT_SECRET)).isEqualTo("secret");
 	}
 
-	@Test
-	public void getCorruptConfiguration_raisesException() {
-		String xsuaaBinding = "{\"xsuaa\": [{ \"credentials\": null }]}";
-
-		assertThatThrownBy(() -> {
-			cut = CFEnvironment.getInstance((str) -> xsuaaBinding, (str) -> null);
-		}).isInstanceOf(JsonParsingException.class).hasMessageContainingAll("The credentials of 'VCAP_SERVICES' can not be parsed for service 'XSUAA'", "Please check the service binding.");
-	}
-
 	// TODO IAS
 	@Test(expected = UnsupportedOperationException.class)
 	public void getConfigurationOfOneIasInstance() {
diff --git a/java-security/src/test/java/com/sap/cloud/security/spring/adapter/SAPOfflineTokenServicesCloudTest.java b/java-security/src/test/java/com/sap/cloud/security/spring/adapter/SAPOfflineTokenServicesCloudTest.java
new file mode 100644
index 00000000..4d73429c
--- /dev/null
+++ b/java-security/src/test/java/com/sap/cloud/security/spring/adapter/SAPOfflineTokenServicesCloudTest.java
@@ -0,0 +1,98 @@
+package com.sap.cloud.security.spring.adapter;
+
+import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
+import com.sap.cloud.security.config.OAuth2ServiceConfigurationBuilder;
+import com.sap.cloud.security.config.Service;
+import com.sap.cloud.security.config.cf.CFConstants;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.validation.ValidationResults;
+import com.sap.cloud.security.token.validation.Validator;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.function.Supplier;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.times;
+
+public class SAPOfflineTokenServicesCloudTest {
+
+	private SAPOfflineTokenServicesCloud cut;
+	private String xsuaaToken;
+
+	public SAPOfflineTokenServicesCloudTest() throws IOException {
+		xsuaaToken = IOUtils.resourceToString("/xsuaaScopesTokenRSA256.txt", StandardCharsets.UTF_8);
+	}
+
+	@Before
+	public void setUp() {
+		cut = createSAPOfflineTokenServicesCloud((token) -> ValidationResults.createValid());
+	}
+
+	@Test
+	public void loadAuthentication() throws IOException {
+		cut.afterPropertiesSet();
+		OAuth2Authentication authentication = cut.loadAuthentication(xsuaaToken);
+
+		assertThat(authentication.isAuthenticated()).isTrue();
+		assertThat(authentication.getOAuth2Request()).isNotNull();
+		assertThat(authentication.getOAuth2Request().getScope()).contains("openid");
+	}
+
+	@Test
+	public void readAccessToken() {
+		assertThatThrownBy(() -> cut.readAccessToken("token")).isInstanceOf(UnsupportedOperationException.class);
+	}
+
+	@Test
+	public void loadAuthentication_notAuthenticated() {
+		cut = createSAPOfflineTokenServicesCloud((token) -> ValidationResults.createInvalid("not valid"));
+		cut.afterPropertiesSet();
+
+		OAuth2Authentication authentication = cut.loadAuthentication(xsuaaToken);
+
+		assertThat(authentication.isAuthenticated()).isFalse();
+	}
+
+	@Test
+	public void afterPropertiesSet() {
+		TestTokenValidatorSupplier mockValidator = Mockito.mock(TestTokenValidatorSupplier.class);
+		cut =  new SAPOfflineTokenServicesCloud(Mockito.mock(OAuth2ServiceConfiguration.class), mockValidator);
+
+		Mockito.verify(mockValidator, times(0)).get();
+		cut.afterPropertiesSet();
+		Mockito.verify(mockValidator, times(1)).get();
+	}
+
+	@Test
+	public void createInstanceWithPublicConstructor_notAuthenticated() {
+		cut =  new SAPOfflineTokenServicesCloud(Mockito.mock(OAuth2ServiceConfiguration.class));
+		cut.afterPropertiesSet();
+		OAuth2Authentication authentication = cut.loadAuthentication(xsuaaToken);
+
+		assertThat(authentication.isAuthenticated()).isFalse();
+	}
+
+	private static class TestTokenValidatorSupplier implements Supplier<Validator<Token>> {
+		@Override public Validator<Token> get() {
+			return null;
+		}
+	}
+
+	private SAPOfflineTokenServicesCloud createSAPOfflineTokenServicesCloud(Validator<Token> tokenValidator) {
+		OAuth2ServiceConfiguration configuration = OAuth2ServiceConfigurationBuilder
+				.forService(Service.XSUAA)
+				.withProperty(CFConstants.XSUAA.APP_ID, "appId")
+				.withProperty(CFConstants.CLIENT_ID, "clientId")
+				.withProperty(CFConstants.XSUAA.UAA_DOMAIN, "localhost")
+				.build();
+		return new SAPOfflineTokenServicesCloud(configuration, () -> tokenValidator);
+	}
+
+}
\ No newline at end of file
diff --git a/spring-xsuaa-test/README.md b/spring-xsuaa-test/README.md
index e9ae509f..5edfb3d6 100644
--- a/spring-xsuaa-test/README.md
+++ b/spring-xsuaa-test/README.md
@@ -27,7 +27,7 @@ This includes for example a `JwtGenerator` that generates JSON Web Tokens (JWT)
 <dependency>
     <groupId>com.sap.cloud.security.xsuaa</groupId>
     <artifactId>spring-xsuaa-test</artifactId>
-    <version>2.4.1-SNAPSHOT</version>
+    <version>2.4.0-SNAPSHOT</version>
     <scope>test</scope>
 </dependency>
 
diff --git a/spring-xsuaa-test/pom.xml b/spring-xsuaa-test/pom.xml
index 8b0f5ed8..8be0cd5e 100644
--- a/spring-xsuaa-test/pom.xml
+++ b/spring-xsuaa-test/pom.xml
@@ -7,7 +7,7 @@
 	<parent>
 		<groupId>com.sap.cloud.security.xsuaa</groupId>
 		<artifactId>parent</artifactId>
-		<version>2.4.1-SNAPSHOT</version>
+		<version>2.4.0-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>spring-xsuaa-test</artifactId>