Repository: SAP/cloud-security-xsuaa-integration
Pull request #231 (2020-02-21 14:46:35)

Buildpack integration changes
This PR are a collection of changes that were necessary to get the build pack integration working.

It does also contain a bug fix for [this method](https://github.com/SAP/cloud-security-xsuaa-integration/compare/BuildpackIntegration?expand=1#diff-6bcade52f5d1e4ee3532b801ab6c0a0eR108) which was not working. I added a method `toJsonString` to the `JsonObject` interface for that.
Another important change is in the `DefaultJsonObject.java` which was implemented with lazy data loading. (Meaning that it only parses the json string when the first method is called which actually reads some data).
This lead to a problem where exceptions were thrown inside (unrelated) validators complaining that the json is corrupt. This is very confusing. It is better to receive this exception as soon as the json object is created. 
I also created a utility class `FileReaderUtil` that reads file from disks (which `JwtGenerator` and `RSAKeys` use). With this it is possible to declare the path of the file inside resources which makes it easier and less error prone to write tests, e.g. [here](https://github.com/SAP/cloud-security-xsuaa-integration/compare/BuildpackIntegration?expand=1#diff-bb3463c258983b0f423ad8d05ed25765L50)

I move the first interface into the API project.

Last but not least this PR adds a new method to the JwtGenerator `withClaimsFromFile` which loads the claims defined in json file into the token that is being created.
I also added documentation.



Patch:
diff --git a/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java b/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java
index dda2bc53..93b75bb3 100644
--- a/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java
+++ b/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java
@@ -16,7 +16,10 @@ import com.sap.cloud.security.xsuaa.Assertions;
 import com.sap.cloud.security.xsuaa.client.SpringOAuth2TokenKeyService;
 import com.sap.cloud.security.xsuaa.client.SpringOidcConfigurationService;
 import org.springframework.beans.factory.InitializingBean;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.oauth2.common.OAuth2AccessToken;
 import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
@@ -27,10 +30,7 @@ import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
 
 import javax.annotation.Nonnull;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -143,18 +143,19 @@ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices
 		if (validationResult.isErroneous()) {
 			throw new InvalidTokenException(validationResult.getErrorDescription());
 		}
+		SecurityContext.setToken(token);
 
 		return getOAuth2Authentication(token, serviceConfiguration.getClientId(), getScopes(token));
 	}
 
 	static OAuth2Authentication getOAuth2Authentication(Token token, String clientId, Set<String> scopes) {
+		Authentication userAuthentication = null; // TODO no SAPUserDetails support. Using spring alternative?
 
-		AuthorizationRequest authorizationRequest = new AuthorizationRequest(new HashMap<>(), null,
-				clientId, scopes.stream().collect(Collectors.toSet()), new HashSet<>(),
-				null,
-				true, "", "", null);
-		SecurityContext.setToken(token);
-		return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), null);
+		final AuthorizationRequest authorizationRequest = new AuthorizationRequest(clientId, scopes);
+		authorizationRequest.setAuthorities(getAuthorities(scopes));
+		authorizationRequest.setApproved(true);
+
+		return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), userAuthentication);
 	}
 
 	private Set<String> getScopes(Token token) {
@@ -192,12 +193,17 @@ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices
 		return this;
 	}
 
+	private static Set<GrantedAuthority> getAuthorities(Collection<String> scopes) {
+		return scopes.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
+	}
+
 	private Token checkAndCreateToken(@Nonnull String accessToken) {
 		try {
 			switch (serviceConfiguration.getService()) {
 			case XSUAA:
 				return new XsuaaToken(accessToken).withScopeConverter(xsuaaScopeConverter);
 			default:
+				// TODO support IAS
 				throw new InvalidTokenException(
 						"AccessToken of service " + serviceConfiguration.getService() + " is not supported.");
 			}
diff --git a/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java b/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
index 7d7649b4..621b3a69 100644
--- a/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
+++ b/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
@@ -181,9 +181,11 @@ public class XSUserInfoAdapter implements XSUserInfo {
 		checkNotGrantTypeClientCredentials("hasAttributes");
 		if (accessToken.hasClaim(EXTERNAL_CONTEXT)) {
 			JsonObject extContext = getClaimAsJsonObject(EXTERNAL_CONTEXT);
-			return extContext.contains(XS_USER_ATTRIBUTES) && !extContext.getJsonObject(EXTERNAL_CONTEXT).isEmpty();
+			return extContext != null && extContext.contains(XS_USER_ATTRIBUTES) && !extContext
+					.getJsonObject(EXTERNAL_CONTEXT).isEmpty();
 		} else {
-			return !getClaimAsJsonObject(XS_USER_ATTRIBUTES).isEmpty();
+			JsonObject xsUserAttributes = getClaimAsJsonObject(XS_USER_ATTRIBUTES);
+			return !(xsUserAttributes == null || xsUserAttributes.isEmpty());
 		}
 	}
 
diff --git a/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java b/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java
index c2ca9409..8a33594b 100644
--- a/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java
+++ b/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java
@@ -3,6 +3,8 @@ package com.sap.cloud.security.json;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
 import java.time.DateTimeException;
@@ -16,17 +18,18 @@ import java.util.*;
  */
 public class DefaultJsonObject implements JsonObject {
 
-	private final String jsonString;
+	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultJsonObject.class);
+
 	private JSONObject jsonObject;
 
 	/**
 	 * Create an instance
-	 * 
+	 *
 	 * @param jsonString
 	 *            the content in json format that should be parsed.
 	 */
 	public DefaultJsonObject(String jsonString) {
-		this.jsonString = jsonString;
+		this.jsonObject = createJsonObject(jsonString);
 	}
 
 	@Override
@@ -107,6 +110,11 @@ public class DefaultJsonObject implements JsonObject {
 		return map;
 	}
 
+	@Override
+	public String asJsonString() {
+		return jsonObject.toString();
+	}
+
 	private List<JsonObject> convertToJsonObjects(JSONArray jsonArray) {
 		List<JsonObject> jsonObjects = new ArrayList<>();
 		jsonArray.forEach(jsonArrayObject -> {
@@ -160,14 +168,17 @@ public class DefaultJsonObject implements JsonObject {
 	}
 
 	private JSONObject getJsonObject() {
-		if (jsonObject == null) {
-			try {
-				jsonObject = new JSONObject(jsonString);
-			} catch (JSONException e) {
-				throw new JsonParsingException(e.getMessage());
-			}
-		}
 		return jsonObject;
 	}
 
+	private JSONObject createJsonObject(String jsonString) {
+		try {
+			JSONObject createdJsonObject = new JSONObject(jsonString);
+			return createdJsonObject;
+		} catch (JSONException e) {
+			LOGGER.error("Given json string '{}' is not valid, error message: {}", jsonString, e.getMessage());
+			throw new JsonParsingException(e.getMessage());
+		}
+	}
+
 }
diff --git a/java-security/src/main/java/com/sap/cloud/security/json/JsonObject.java b/java-security/src/main/java/com/sap/cloud/security/json/JsonObject.java
index 7f154e60..5feed078 100644
--- a/java-security/src/main/java/com/sap/cloud/security/json/JsonObject.java
+++ b/java-security/src/main/java/com/sap/cloud/security/json/JsonObject.java
@@ -117,4 +117,10 @@ public interface JsonObject {
 	 * @return the json properties as key-value map
 	 */
 	Map<String, String> getKeyValueMap();
+
+	/**
+	 * Returns the json object as a json string.
+	 * @return the json object in string representation.
+	 */
+	String asJsonString();
 }
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
index 297a006e..71a25a25 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
@@ -32,6 +32,9 @@ public class SpringOAuth2TokenKeyService implements OAuth2TokenKeyService {
 		} catch (HttpClientErrorException ex) {
 			throw OAuth2ServiceException.createWithStatusCodeAndResponseBody("Error retrieving token keys",
 					ex.getStatusCode().value(), ex.getResponseBodyAsString());
+		} catch (Exception e) {
+			throw OAuth2ServiceException.createWithStatusCodeAndResponseBody("Unexpected error retrieving token keys",
+					500, e.getMessage());
 		}
 	}
 


Test patch:
diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java b/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java
index f4f7f3a0..bece938e 100644
--- a/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java
+++ b/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java
@@ -17,6 +17,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.security.*;
 import java.time.Instant;
 import java.util.*;
@@ -98,12 +101,13 @@ public class JwtGenerator {
 	 * @param object
 	 *            the string value of the claim to be set.
 	 * @return the builder object.
+	 * @throws JsonParsingException
+	 *             if the given object does not contain valid json.
 	 */
 	public JwtGenerator withClaimValue(String claimName, JsonObject object) {
 		assertClaimIsSupported(claimName);
 		try {
-			JSONObject wrappedObject = new JSONObject(object.toString());
-			jsonPayload.put(claimName, wrappedObject);
+			jsonPayload.put(claimName, new JSONObject(object.asJsonString()));
 		} catch (JSONException e) {
 			throw new JsonParsingException(e.getMessage());
 		}
@@ -125,6 +129,33 @@ public class JwtGenerator {
 		return this;
 	}
 
+	/**
+	 * This method will fill the token with all the claims that are defined inside
+	 * the given file. The file must contain a valid json object.
+	 *
+	 * @throws JsonParsingException
+	 *             if the file does not contain a valid json object.
+	 * @throws IOException
+	 *             when the file cannot be read or does not exist.
+	 * @param claimsJsonFilePath
+	 *            the file path to the file containing the claims in json format.
+	 * @return the builder object.
+	 */
+	public JwtGenerator withClaimsFromFile(String claimsJsonFilePath) throws IOException {
+		String claimsJson = new String(Files.readAllBytes(Paths.get(claimsJsonFilePath)));
+		JSONObject claimsAsJsonObject;
+		try {
+			claimsAsJsonObject = new JSONObject(claimsJson);
+		} catch (JSONException e) {
+			throw new JsonParsingException(e.getMessage());
+		}
+		for (String key : claimsAsJsonObject.keySet()) {
+			Object value = claimsAsJsonObject.get(key);
+			jsonPayload.put(key, value);
+		}
+		return this;
+	}
+
 	private void assertClaimIsSupported(String claimName) {
 		if (unsupportedClaims.contains(claimName)) {
 			throw new UnsupportedOperationException("generic method for claim " + claimName + " is not supported");
diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/RSAKeys.java b/java-security-test/src/main/java/com/sap/cloud/security/test/RSAKeys.java
index 080aec9a..b51d85f4 100644
--- a/java-security-test/src/main/java/com/sap/cloud/security/test/RSAKeys.java
+++ b/java-security-test/src/main/java/com/sap/cloud/security/test/RSAKeys.java
@@ -2,15 +2,16 @@ package com.sap.cloud.security.test;
 
 import java.io.IOException;
 import java.nio.file.Files;
-import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.security.*;
 import java.security.spec.InvalidKeySpecException;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.security.spec.X509EncodedKeySpec;
 import java.util.Base64;
-import java.util.stream.Collectors;
 
+/**
+ * Utility class to create RSAKeys used for testing.
+ */
 public class RSAKeys {
 
 	private static final String RSA = "RSA";
@@ -18,39 +19,84 @@ public class RSAKeys {
 
 	private final KeyPair keyPair;
 
+	private RSAKeys(KeyPair keyPair) {
+		this.keyPair = keyPair;
+	}
+
+	/**
+	 * Generates a random RSA keypair.
+	 *
+	 * @return the instance.
+	 */
 	public static RSAKeys generate() {
 		KeyPair theKeyPair = generateKeyPair();
 		return new RSAKeys(new KeyPair(theKeyPair.getPublic(), theKeyPair.getPrivate()));
 	}
 
+	/**
+	 * Creates an instance with the given key pair. For more information see
+	 * {@link RSAKeys#loadPublicKey(String)} and
+	 * {@link RSAKeys#loadPrivateKey(String)}.
+	 *
+	 * @param publicKeyPath
+	 *            the path to the key file.
+	 * @param privateKeyPath
+	 *            the path to the key file.
+	 * @return the instance.
+	 * @throws IOException
+	 *             in case the files could not be read.
+	 * @throws NoSuchAlgorithmException
+	 *             see {@link KeyFactory#getInstance(java.lang.String)}.
+	 * @throws InvalidKeySpecException
+	 *             see
+	 *             {@link KeyFactory#generatePublic(java.security.spec.KeySpec)}.
+	 */
 	public static RSAKeys fromKeyFiles(String publicKeyPath, String privateKeyPath)
 			throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
 		KeyPair keyPair = new KeyPair(loadPublicKey(publicKeyPath), loadPrivateKey(privateKeyPath));
 		return new RSAKeys(keyPair);
 	}
 
-	public PublicKey getPublic() {
-		return keyPair.getPublic();
-	}
-
-	public PrivateKey getPrivate() {
-		return keyPair.getPrivate();
-	}
-
-	private RSAKeys(KeyPair keyPair) {
-		this.keyPair = keyPair;
-	}
-
+	/**
+	 * Loads the public key from the given file. The key is expected to be encoded
+	 * according to the X.509 standard and in base64 format (pem).
+	 *
+	 * @param publicKeyPath
+	 *            the path to the key file.
+	 * @return the {@link PublicKey} instance.
+	 * @throws IOException
+	 *             in case the file could not be read.
+	 * @throws NoSuchAlgorithmException
+	 *             see {@link KeyFactory#getInstance(java.lang.String)}.
+	 * @throws InvalidKeySpecException
+	 *             see
+	 *             {@link KeyFactory#generatePublic(java.security.spec.KeySpec)}.
+	 */
 	public static PublicKey loadPublicKey(String publicKeyPath)
 			throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
-		String publicKeyString = readFileAndReplaceBeginEnd(Paths.get(publicKeyPath));
+		String publicKeyString = readFileAndReplaceBeginEnd(publicKeyPath);
 		X509EncodedKeySpec spec = new X509EncodedKeySpec(base64Decode(publicKeyString));
 		return KeyFactory.getInstance(RSA).generatePublic(spec);
 	}
 
+	/**
+	 * Loads the private key from the given file. The key is expected to be
+	 * according to PKCS #8 standard and in base64 format (pem).
+	 *
+	 * @param privateKeyPath
+	 *            the path to the key file.
+	 * @return the {@link PrivateKey} instance.
+	 * @throws IOException
+	 *             in case the file could not be read.
+	 * @throws NoSuchAlgorithmException
+	 *             see {@link KeyFactory#getInstance(java.lang.String)}.
+	 * @throws InvalidKeySpecException
+	 *             see
+	 *             {@link KeyFactory#generatePublic(java.security.spec.KeySpec)}.
+	 */
 	public static PrivateKey loadPrivateKey(String privateKeyPath)
 			throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
-		String privateKeyString = readFileAndReplaceBeginEnd(Paths.get(privateKeyPath));
+		String privateKeyString = readFileAndReplaceBeginEnd(privateKeyPath);
 		PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(base64Decode(privateKeyString));
 		return KeyFactory.getInstance(RSA).generatePrivate(spec);
 	}
@@ -59,9 +105,10 @@ public class RSAKeys {
 		return Base64.getDecoder().decode(publicKeyString);
 	}
 
-	private static String readFileAndReplaceBeginEnd(Path filePath) throws IOException {
-		String content = Files.readAllLines(filePath).stream().collect(Collectors.joining(""));
-		return content.replaceAll(KEY_BEGIN_END_REGEX, "");
+	private static String readFileAndReplaceBeginEnd(String filePath) throws IOException {
+		String content = new String(Files.readAllBytes(Paths.get(filePath)));
+		String newLinesRemoved = content.replaceAll("\\r|\\n", "");
+		return newLinesRemoved.replaceAll(KEY_BEGIN_END_REGEX, "");
 	}
 
 	private static KeyPair generateKeyPair() {
@@ -72,4 +119,20 @@ public class RSAKeys {
 		}
 	}
 
+	/**
+	 *
+	 * @return the public key of the pair.
+	 */
+	public PublicKey getPublic() {
+		return keyPair.getPublic();
+	}
+
+	/**
+	 *
+	 * @return the private key of the pair.
+	 */
+	public PrivateKey getPrivate() {
+		return keyPair.getPrivate();
+	}
+
 }
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 0e1cfe32..853c11bb 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
@@ -4,21 +4,29 @@ import com.sap.cloud.security.config.Environments;
 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.json.DefaultJsonObject;
+import com.sap.cloud.security.json.JsonObject;
+import com.sap.cloud.security.json.JsonParsingException;
 import com.sap.cloud.security.token.Token;
 import com.sap.cloud.security.token.TokenClaims;
 import com.sap.cloud.security.token.TokenHeader;
-import com.sap.cloud.security.token.validation.ValidationResult;
 import com.sap.cloud.security.token.validation.CombiningValidator;
+import com.sap.cloud.security.token.validation.ValidationResult;
 import com.sap.cloud.security.token.validation.validators.JwtValidatorBuilder;
-import com.sap.cloud.security.xsuaa.client.*;
+import com.sap.cloud.security.xsuaa.client.OAuth2ServiceEndpointsProvider;
+import com.sap.cloud.security.xsuaa.client.OAuth2TokenKeyService;
+import com.sap.cloud.security.xsuaa.client.OidcConfigurationService;
 import org.apache.commons.io.IOUtils;
 import org.junit.*;
+import org.junit.rules.TemporaryFolder;
 import org.mockito.Mockito;
 import sun.security.rsa.RSAPublicKeyImpl;
 
+import java.io.File;
 import java.io.IOException;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Paths;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
@@ -26,6 +34,7 @@ import java.time.Instant;
 import java.time.LocalDate;
 import java.time.ZoneOffset;
 import java.util.Base64;
+import java.util.List;
 import java.util.Properties;
 
 import static com.sap.cloud.security.config.Service.IAS;
@@ -43,10 +52,13 @@ public class JwtGeneratorTest {
 	private JwtGenerator cut;
 	private Properties originalSystemProperties;
 
+	@ClassRule
+	public static TemporaryFolder temporaryFolder = new TemporaryFolder(Paths.get(".").toFile());
+
 	@BeforeClass
 	public static void setUpClass() throws Exception {
-		String publicKeyPath = IOUtils.resourceToURL("/publicKey.txt").getPath();
-		String privateKeyPath = IOUtils.resourceToURL("/privateKey.txt").getPath();
+		String publicKeyPath = getPathToResourcesFile("/publicKey.txt");
+		String privateKeyPath = getPathToResourcesFile("/privateKey.txt");
 		keys = RSAKeys.fromKeyFiles(publicKeyPath, privateKeyPath);
 	}
 
@@ -174,6 +186,83 @@ public class JwtGeneratorTest {
 				.isInstanceOf(UnsupportedOperationException.class);
 	}
 
+	@Test
+	public void withClaimValue_asJsonObjectContainingString() {
+		Token token = cut.withClaimValue("key1", new DefaultJsonObject("{\"key2\" : \"abc\"}"))
+				.createToken();
+
+		JsonObject object = token.getClaimAsJsonObject("key1");
+		assertThat(object).isNotNull();
+		assertThat(object.getAsString("key2")).isEqualTo("abc");
+	}
+
+	@Test
+	public void withClaimValue_asJsonObjectContainingJsonObject() {
+		Token token = cut.withClaimValue("key1", new DefaultJsonObject("{\"key2\" : {\"key3\": \"theValue\"}}"))
+				.createToken();
+
+		JsonObject object = token.getClaimAsJsonObject("key1");
+		assertThat(object).isNotNull();
+		JsonObject innerObject = object.getJsonObject("key2");
+		assertThat(innerObject).isNotNull();
+		assertThat(innerObject.getAsString("key3")).isEqualTo("theValue");
+	}
+
+	@Test
+	public void withClaimValue_asJsonObjectContainingList() {
+		Token token = cut.withClaimValue("key1", new DefaultJsonObject("{\"key2\": [\"a\", \"b\"]}"))
+				.createToken();
+
+		JsonObject object = token.getClaimAsJsonObject("key1");
+		assertThat(object).isNotNull();
+		List<String> list = object.getAsList("key2", String.class);
+		assertThat(list).containsExactly("a", "b");
+	}
+
+	@Test
+	public void loadClaimsFromFile_doesNotContainValidJson_throwsException() throws IOException {
+		File emptyFile = temporaryFolder.newFile("empty");
+
+		assertThatThrownBy(() -> cut.withClaimsFromFile(emptyFile.getPath()).createToken())
+				.isInstanceOf(JsonParsingException.class);
+	}
+
+	@Test
+	public void loadClaimsFromFile_containsStringClaims() throws IOException {
+		final Token token = cut.withClaimsFromFile(getPathToResourcesFile("/claims.json")).createToken();
+
+		assertThat(token.getClaimAsString(TokenClaims.EMAIL)).isEqualTo("test@uaa.org");
+		assertThat(token.getClaimAsString(TokenClaims.XSUAA.GRANT_TYPE))
+				.isEqualTo("urn:ietf:params:oauth:grant-type:saml2-bearer");
+	}
+
+	@Test
+	public void loadClaimsFromFile_containsExpirationClaim() throws IOException {
+		final Token token = cut.withClaimsFromFile(getPathToResourcesFile("/claims.json")).createToken();
+
+		assertThat(token.getExpiration()).isEqualTo(Instant.ofEpochSecond(1542416800));
+	}
+
+	@Test
+	public void loadClaimsFromFile_containsJsonObjectClaims() throws IOException {
+		final Token token = cut.withClaimsFromFile(getPathToResourcesFile("/claims.json")).createToken();
+
+		JsonObject externalAttributes = token.getClaimAsJsonObject("ext_attr");
+
+		assertThat(externalAttributes).isNotNull();
+		assertThat(externalAttributes.getAsString("enhancer")).isEqualTo("XSUAA");
+		assertThat(externalAttributes.getAsList("acl", String.class)).containsExactly("app1!t23");
+	}
+
+	@Test
+	public void loadClaimsFromFile_containsListClaims() throws IOException {
+		final Token token = cut.withClaimsFromFile(getPathToResourcesFile("/claims.json")).createToken();
+
+		assertThat(token.getClaimAsStringList(TokenClaims.XSUAA.SCOPES))
+				.containsExactly("openid", "testScope", "testApp.localScope");
+		assertThat(token.getClaimAsStringList("empty_list")).isEmpty();
+	}
+
 	@Test
 	public void createToken_signatureCalculation_NoSuchAlgorithmExceptionTurnedIntoRuntimeException() {
 		cut = JwtGenerator.getInstance(XSUAA, (key, alg, data) -> {
@@ -253,4 +342,8 @@ public class JwtGeneratorTest {
 		assertThat(result.isValid()).isTrue();
 	}
 
+	private static String getPathToResourcesFile(String filePathInResources) throws IOException {
+		return IOUtils.resourceToURL(filePathInResources).getPath();
+	}
+
 }
diff --git a/java-security-test/src/test/java/com/sap/cloud/security/test/RSAKeysTest.java b/java-security-test/src/test/java/com/sap/cloud/security/test/RSAKeysTest.java
index 67d49b21..90567b87 100644
--- a/java-security-test/src/test/java/com/sap/cloud/security/test/RSAKeysTest.java
+++ b/java-security-test/src/test/java/com/sap/cloud/security/test/RSAKeysTest.java
@@ -4,7 +4,6 @@ import org.apache.commons.io.IOUtils;
 import org.junit.Test;
 
 import java.io.IOException;
-import java.net.URISyntaxException;
 import java.security.NoSuchAlgorithmException;
 import java.security.spec.InvalidKeySpecException;
 
diff --git a/java-security-test/src/test/resources/claims.json b/java-security-test/src/test/resources/claims.json
new file mode 100644
index 00000000..f3d62728
--- /dev/null
+++ b/java-security-test/src/test/resources/claims.json
@@ -0,0 +1,16 @@
+{
+  "ext_attr": {
+    "enhancer": "XSUAA",
+    "serviceinstanceid": "brokerCloneServiceInstanceId",
+    "zdn": "uaa",
+    "acl": ["app1!t23"]
+  },
+  "scope": [
+    "openid", "testScope", "testApp.localScope"
+  ],
+  "empty_list" : [],
+  "grant_type": "urn:ietf:params:oauth:grant-type:saml2-bearer",
+  "email": "test@uaa.org",
+  "exp": 1542416800,
+  "az_attr": {"external_id":"abcd1234"}
+}
\ No newline at end of file
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java
index c4838778..a09302d6 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java
@@ -12,11 +12,14 @@ import org.apache.commons.io.IOUtils;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mockito;
+import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
 import org.springframework.security.oauth2.provider.OAuth2Authentication;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -57,7 +60,11 @@ public class SAPOfflineTokenServicesCloudTest {
 
 		assertThat(authentication.isAuthenticated()).isTrue();
 		assertThat(authentication.getOAuth2Request()).isNotNull();
-		assertThat(authentication.getOAuth2Request().getScope()).contains("ROLE_SERVICEBROKER", "uaa.resource");
+		assertThat(authentication.getOAuth2Request().getScope()).containsExactlyInAnyOrder("ROLE_SERVICEBROKER", "uaa.resource");
+		Collection<String> authorities = authentication.getOAuth2Request().getAuthorities().stream()
+				.map(GrantedAuthority::getAuthority).collect(
+						Collectors.toList());
+		assertThat(authorities).containsExactlyInAnyOrder("ROLE_SERVICEBROKER", "uaa.resource");
 		assertThat(SecurityContext.getToken().getTokenValue()).isEqualTo(xsuaaToken);
 	}
 
@@ -68,8 +75,7 @@ public class SAPOfflineTokenServicesCloudTest {
 		cut.afterPropertiesSet();
 
 		OAuth2Authentication authentication = cut.loadAuthentication(xsuaaToken);
-		assertThat(authentication.getOAuth2Request().getScope()).containsExactly("testApp.localScope", "openid",
-				"testScope");
+		assertThat(authentication.getOAuth2Request().getScope()).containsExactlyInAnyOrder("testApp.localScope", "openid", "testScope");
 
 		cut.setLocalScopeAsAuthorities(true);
 		authentication = cut.loadAuthentication(xsuaaToken);
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
index a180bfec..4814f819 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
@@ -5,7 +5,10 @@ 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.json.JsonObject;
-import com.sap.cloud.security.token.*;
+import com.sap.cloud.security.token.GrantType;
+import com.sap.cloud.security.token.TokenClaims;
+import com.sap.cloud.security.token.XsuaaScopeConverter;
+import com.sap.cloud.security.token.XsuaaToken;
 import com.sap.xsa.security.container.XSUserInfoException;
 import org.apache.commons.io.IOUtils;
 import org.junit.Before;
@@ -136,6 +139,14 @@ public class XSUserInfoAdapterTest {
 		assertThat(cut.getDBToken()).isEqualTo(cut.getAppToken());
 	}
 
+	@Test
+	public void testGetDBToken_onEmptyToken_throwsException() throws XSUserInfoException {
+		cut = new XSUserInfoAdapter(emptyToken);
+
+		assertThatThrownBy(() -> cut.getDBToken()).isInstanceOf(XSUserInfoException.class);
+		assertThatThrownBy(() -> cut.getHdbToken()).isInstanceOf(XSUserInfoException.class);
+	}
+
 	@Test
 	public void testGetHdbToken() throws XSUserInfoException {
 		assertThat(cut.getHdbToken()).isEqualTo(cut.getAppToken());
@@ -187,10 +198,16 @@ public class XSUserInfoAdapterTest {
 	}
 
 	@Test
-	public void testHasAttributes() throws XSUserInfoException {
+	public void testHasAttributes_true() throws XSUserInfoException {
 		assertThat(cut.hasAttributes()).isTrue();
 	}
 
+	@Test
+	public void testHasAttributes_false() throws XSUserInfoException {
+		cut = new XSUserInfoAdapter(emptyToken);
+		assertThat(cut.hasAttributes()).isFalse();
+	}
+
 	@Test
 	public void testHasAttributes_grantTypeClientCredentials_throwsException() throws XSUserInfoException {
 		cut = new XSUserInfoAdapter(createMockToken(GrantType.CLIENT_CREDENTIALS));
diff --git a/java-security/src/test/java/com/sap/cloud/security/json/DefaultJsonObjectTest.java b/java-security/src/test/java/com/sap/cloud/security/json/DefaultJsonObjectTest.java
index 27a3fce9..0d43b5f9 100644
--- a/java-security/src/test/java/com/sap/cloud/security/json/DefaultJsonObjectTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/json/DefaultJsonObjectTest.java
@@ -47,6 +47,11 @@ public class DefaultJsonObjectTest {
 		assertThat(new DefaultJsonObject("{}").isEmpty()).isTrue();
 	}
 
+	@Test
+	public void createWithmalformedJsonString_throwsException() {
+		assertThatThrownBy(() -> new DefaultJsonObject("")).isInstanceOf(JsonParsingException.class);
+	}
+
 	@Test
 	public void getAsString_keyExists_returnsStringValue() {
 		assertThat(cut.getAsString(KEY_1)).isEqualTo(STRING_TEXT);
@@ -170,13 +175,6 @@ public class DefaultJsonObjectTest {
 		assertThatThrownBy(() -> cut.getJsonObjects(KEY_1)).isInstanceOf(JsonParsingException.class);
 	}
 
-	@Test
-	public void getJsonObjects_malformedJson_throwsException() {
-		cut = new DefaultJsonObject("");
-
-		assertThatThrownBy(() -> cut.getJsonObject(KEY_1)).isInstanceOf(JsonParsingException.class);
-	}
-
 	private DefaultJsonObject createJsonParser(String key, Object value) {
 		String jsonString = createJsonObjectString(key, value);
 		return new DefaultJsonObject(jsonString);