Repository: SAP/cloud-security-xsuaa-integration
Pull request #267 (2020-04-20 15:40:11)

User info compatibility
This PR improves UserInfo compatibility. I have added an integration test that consists of the original UserInfo test and its test resources.
I have also synchronised the deprecation annotations on the XSUserInfo interface. 

I just noticed that there is still one thing be missing when using the `requestToken` method of `UserInfo`: We do not provide an implementation of `XSTokenRequest`. Callers of this method would need to provide their own implementation.

Should we provide an implementation in the library? If so where and should this also be part of this PR?


Patch:
diff --git a/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java b/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java
index 2dbdb596..e69a19ad 100644
--- a/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java
+++ b/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java
@@ -133,6 +133,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
+	 @Deprecated
 	public String getJsonValue(String attribute) throws XSUserInfoException;
 
 	/**
@@ -154,6 +155,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if db token is not available in the authentication token
 	 */
+	@Deprecated
 	public String getDBToken() throws XSUserInfoException;
 
 	/**
@@ -187,6 +189,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if token is not available in the authentication token
 	 */
+	 @Deprecated
 	public String getToken(String namespace, String name) throws XSUserInfoException;
 
 	/**
@@ -237,7 +240,6 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	@Deprecated
 	public boolean checkScope(String scope) throws XSUserInfoException;
 
 	/**
@@ -253,7 +255,6 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	@Deprecated
 	public boolean checkLocalScope(String scope) throws XSUserInfoException;
 
 	/**
@@ -334,7 +335,6 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if an error occurs during token exchange
 	 */
-	@Deprecated
 	public String requestToken(XSTokenRequest tokenRequest) throws XSUserInfoException;
 
 }
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 0ef8a879..214ffbf9 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
@@ -7,8 +7,11 @@ import com.sap.cloud.security.json.JsonObject;
 import com.sap.cloud.security.json.JsonParsingException;
 import com.sap.cloud.security.token.AccessToken;
 import com.sap.cloud.security.token.GrantType;
-import com.sap.cloud.security.token.Token;
 import com.sap.cloud.security.token.TokenClaims;
+import com.sap.cloud.security.xsuaa.Assertions;
+import com.sap.cloud.security.xsuaa.client.*;
+import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
+import com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlows;
 import com.sap.xsa.security.container.XSTokenRequest;
 import com.sap.xsa.security.container.XSUserInfo;
 import com.sap.xsa.security.container.XSUserInfoException;
@@ -16,6 +19,8 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Supplier;
@@ -23,6 +28,15 @@ import java.util.function.Supplier;
 import static com.sap.cloud.security.token.TokenClaims.*;
 import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
 
+/**
+ * This class implements the {@link XSUserInfo} interface by wrapping and
+ * delegating calls to an {@link AccessToken}.
+ *
+ * Other implementations of {@link XSUserInfo} support loading the token from
+ * the the spring security context holder. This is not supported by this class!
+ * It also does not support the SAPAuthorizationExtension.
+ *
+ */
 public class XSUserInfoAdapter implements XSUserInfo {
 
 	static final String EXTERNAL_CONTEXT = "ext_ctx";
@@ -34,23 +48,25 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	static final String ZDN = "zdn";
 	static final String SYSTEM = "SYSTEM";
 	static final String HDB = "HDB";
+
 	private static final Logger LOGGER = LoggerFactory.getLogger(XSUserInfoAdapter.class);
-	private final AccessToken accessToken;
-	private OAuth2ServiceConfiguration configuration;
 
-	public XSUserInfoAdapter(Object accessToken) throws XSUserInfoException {
-		this(accessToken, null);
-	}
+	private final AccessToken accessToken;
+	private final OAuth2ServiceConfiguration configuration;
+	/**
+	 * Use {@link #getOrCreateOAuth2TokenService()} for access.
+	 */
+	private OAuth2TokenService oAuth2TokenService;
 
-	public XSUserInfoAdapter(Token accessToken) throws XSUserInfoException {
+	public XSUserInfoAdapter(Object accessToken) {
 		this(accessToken, Environments.getCurrent().getXsuaaConfiguration());
 	}
 
-	public XSUserInfoAdapter(AccessToken accessToken) throws XSUserInfoException {
-		this(accessToken, null);
+	public XSUserInfoAdapter(AccessToken accessToken) {
+		this(accessToken, Environments.getCurrent().getXsuaaConfiguration());
 	}
 
-	XSUserInfoAdapter(Object accessToken, OAuth2ServiceConfiguration configuration) throws XSUserInfoException {
+	XSUserInfoAdapter(Object accessToken, OAuth2ServiceConfiguration configuration) {
 		if (!(accessToken instanceof AccessToken)) {
 			String type = Objects.isNull(accessToken) ? null : accessToken.getClass().getName();
 			throw new XSUserInfoException("token is of instance " + type
@@ -60,14 +76,25 @@ public class XSUserInfoAdapter implements XSUserInfo {
 		this.configuration = configuration;
 	}
 
+	/*
+	 * Loading the token from the security context like this is not supported!
+	 *
+	 * XSUserInfoAdapter() { Authentication auth =
+	 * SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof
+	 * OAuth2Authentication) { SAPAuthorizationExtension extension =
+	 * (SAPAuthorizationExtension) ((OAuth2Authentication)
+	 * auth).getOAuth2Request().getExtensions().get("sap"); if (extension != null) {
+	 * this.foreignMode = extension.isForeignMode(); } } }
+	 */
+
 	@Override
-	public String getLogonName() throws XSUserInfoException {
+	public String getLogonName() {
 		checkNotGrantTypeClientCredentials("getLogonName");
 		return getClaimValue(USER_NAME);
 	}
 
 	@Override
-	public String getGivenName() throws XSUserInfoException {
+	public String getGivenName() {
 		checkNotGrantTypeClientCredentials("getGivenName");
 		String externalAttributeName = getExternalAttribute(GIVEN_NAME);
 		if (externalAttributeName == null) {
@@ -78,7 +105,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Override
-	public String getFamilyName() throws XSUserInfoException {
+	public String getFamilyName() {
 		checkNotGrantTypeClientCredentials("getFamilyName");
 		String externalAttributeName = getExternalAttribute(FAMILY_NAME);
 		if (externalAttributeName == null) {
@@ -89,18 +116,18 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Override
-	public String getOrigin() throws XSUserInfoException {
+	public String getOrigin() {
 		checkNotGrantTypeClientCredentials("getOrigin");
 		return getClaimValue(ORIGIN);
 	}
 
 	@Override
-	public String getIdentityZone() throws XSUserInfoException {
+	public String getIdentityZone() {
 		return getClaimValue(TokenClaims.XSUAA.ZONE_ID);
 	}
 
 	@Override
-	public String getSubaccountId() throws XSUserInfoException {
+	public String getSubaccountId() {
 		return getIdentityZone();
 	}
 
@@ -108,33 +135,33 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	/**
 	 * "ext_attr": { "enhancer": "XSUAA", "zdn": "paas-subdomain" },
 	 */
-	public String getSubdomain() throws XSUserInfoException {
+	public String getSubdomain() {
 		return Optional.ofNullable(getExternalAttribute(ZDN)).orElse(null);
 	}
 
 	@Override
-	public String getClientId() throws XSUserInfoException {
+	public String getClientId() {
 		return getClaimValue(CLIENT_ID);
 	}
 
 	@Override
-	public String getJsonValue(String attribute) throws XSUserInfoException {
+	public String getJsonValue(String attribute) {
 		return getClaimValue(attribute);
 	}
 
 	@Override
-	public String getEmail() throws XSUserInfoException {
+	public String getEmail() {
 		checkNotGrantTypeClientCredentials("getEmail");
 		return getClaimValue(EMAIL);
 	}
 
 	@Override
-	public String getDBToken() throws XSUserInfoException {
+	public String getDBToken() {
 		return getHdbToken();
 	}
 
 	@Override
-	public String getHdbToken() throws XSUserInfoException {
+	public String getHdbToken() {
 		return getToken(SYSTEM, HDB);
 	}
 
@@ -144,7 +171,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Override
-	public String getToken(String namespace, String name) throws XSUserInfoException {
+	public String getToken(String namespace, String name) {
 		if (!(getGrantType().equals(GrantType.CLIENT_CREDENTIALS.toString())) && hasAttributes() && isInForeignMode()) {
 			throw new XSUserInfoException("The SecurityContext has been initialized with an access token of a\n"
 					+ "foreign OAuth Client Id and/or Identity Zone. Furthermore, the\n"
@@ -174,13 +201,13 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Override
-	public String[] getAttribute(String attributeName) throws XSUserInfoException {
+	public String[] getAttribute(String attributeName) {
 		checkNotGrantTypeClientCredentials("getAttribute");
 		return getMultiValueAttributeFromExtObject(XS_USER_ATTRIBUTES, attributeName);
 	}
 
 	@Override
-	public boolean hasAttributes() throws XSUserInfoException {
+	public boolean hasAttributes() {
 		checkNotGrantTypeClientCredentials("hasAttributes");
 		if (accessToken.hasClaim(EXTERNAL_CONTEXT)) {
 			JsonObject extContext = getClaimAsJsonObject(EXTERNAL_CONTEXT);
@@ -193,17 +220,17 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Override
-	public String[] getSystemAttribute(String attributeName) throws XSUserInfoException {
+	public String[] getSystemAttribute(String attributeName) {
 		return getMultiValueAttributeFromExtObject(XS_SYSTEM_ATTRIBUTES, attributeName);
 	}
 
 	@Override
-	public boolean checkScope(String scope) throws XSUserInfoException {
+	public boolean checkScope(String scope) {
 		return accessToken.hasScope(scope);
 	}
 
 	@Override
-	public boolean checkLocalScope(String scope) throws XSUserInfoException {
+	public boolean checkLocalScope(String scope) {
 		try {
 			return accessToken.hasLocalScope(scope);
 		} catch (IllegalArgumentException e) {
@@ -212,19 +239,19 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Override
-	public String getAdditionalAuthAttribute(String attributeName) throws XSUserInfoException {
+	public String getAdditionalAuthAttribute(String attributeName) {
 		return Optional.ofNullable(getAttributeFromClaimAsString(CLAIM_ADDITIONAL_AZ_ATTR, attributeName))
 				.orElseThrow(createXSUserInfoException(attributeName));
 	}
 
 	@Override
-	public String getCloneServiceInstanceId() throws XSUserInfoException {
+	public String getCloneServiceInstanceId() {
 		return Optional.ofNullable(getExternalAttribute(SERVICEINSTANCEID))
 				.orElseThrow(createXSUserInfoException(SERVICEINSTANCEID));
 	}
 
 	@Override
-	public String getGrantType() throws XSUserInfoException {
+	public String getGrantType() {
 		return Optional.ofNullable(accessToken.getGrantType())
 				.map(GrantType::toString)
 				.orElseThrow(createXSUserInfoException(GRANT_TYPE));
@@ -234,6 +261,8 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	 * Check if a token issued for another OAuth client has been forwarded to a
 	 * different client,
 	 *
+	 * This method does not support checking if the token can be accepted by ACL.
+	 *
 	 * @return true if token was forwarded or if it cannot be determined.
 	 */
 	@Override
@@ -254,7 +283,9 @@ public class XSUserInfoAdapter implements XSUserInfo {
 		boolean identityZonesMatch = tokenIdentityZone
 				.equals(configuration.getProperty(CFConstants.XSUAA.IDENTITY_ZONE));
 		boolean isApplicationPlan = tokenClientId.contains("!t");
-		if (clientIdsMatch && (identityZonesMatch || isApplicationPlan)) {
+		boolean isBrokerPlan = tokenClientId.contains("!b");
+
+		if (clientIdsMatch && (identityZonesMatch || isApplicationPlan || isBrokerPlan)) {
 			LOGGER.info(
 					"Token not in foreign mode because because client ids  match and identityZonesMatch={}, isApplicationPlan={} ",
 					identityZonesMatch, isApplicationPlan);
@@ -273,17 +304,85 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Override
-	public String requestTokenForClient(String clientId, String clientSecret, String uaaUrl) {
-		throw new UnsupportedOperationException("Not implemented. Use token-client library instead.");
+	public String requestTokenForClient(String clientId, String clientSecret, String baseUaaUrl) {
+		return performTokenFlow(baseUaaUrl, XSTokenRequest.TYPE_USER_TOKEN, clientId, clientSecret, new HashMap<>());
 	}
 
 	@Override
-	public String requestToken(XSTokenRequest tokenRequest) throws XSUserInfoException {
-		throw new UnsupportedOperationException("Not implemented. Use token-client library instead.");
+	public String requestToken(XSTokenRequest tokenRequest) {
+		Assertions.assertNotNull(tokenRequest, "TokenRequest argument is required");
+		if (!tokenRequest.isValid()) {
+			throw new XSUserInfoException("Invalid grant type or missing parameters for requested grant type.");
+		}
+		String tokenEndpoint = tokenRequest.getTokenEndpoint().toString();
+		String baseUrl = tokenEndpoint.replace(tokenRequest.getTokenEndpoint().getPath(), "");
+		Map<String, String> additionalAuthAttributes = tokenRequest.getAdditionalAuthorizationAttributes();
+		return performTokenFlow(baseUrl, tokenRequest.getType(), tokenRequest.getClientId(),
+				tokenRequest.getClientSecret(), additionalAuthAttributes);
+	}
+
+	/**
+	 * Tries to create an OAuth2TokenService and throws UnsupportedOperationException if it fails.
+	 *
+	 * @throws UnsupportedOperationException if it cannot create the service.
+	 * @return the created OAuth2TokenService
+	 */
+	private OAuth2TokenService getOrCreateOAuth2TokenService() {
+		if (oAuth2TokenService == null) {
+			oAuth2TokenService = tryToCreateDefaultOAuth2TokenService();
+			if (oAuth2TokenService == null) {
+				oAuth2TokenService = tryToCreateXsuaaOAuth2TokenService();
+			}
+		}
+		if (oAuth2TokenService == null) {
+			throw new UnsupportedOperationException("Failed to create OAuth2TokenService. "
+					+ "Make sure your project has a dependency to either spring-web or apache HTTP client.");
+		}
+		return oAuth2TokenService;
+	}
+
+	/**
+	 * This method tries to create a {@link DefaultOAuth2TokenService} instance
+	 * which can fail because the required dependency (apache HTTP client) might
+	 * be missing. In this case a {@link java.lang.NoClassDefFoundError} is
+	 * thrown which is a {@link LinkageError} that needs to be caught
+	 * in addition to exceptions!
+	 *
+	 * @return the {@link DefaultOAuth2TokenService} instance or null if it could not be created.
+	 */
+	private OAuth2TokenService tryToCreateDefaultOAuth2TokenService() {
+		LOGGER.debug("Trying to create DefaultOAuth2TokenService.");
+		try {
+			return new DefaultOAuth2TokenService();
+		} catch (Exception | LinkageError e) {
+			LOGGER.debug("Failed to create DefaultOAuth2TokenService.", e);
+		}
+		return null;
+	}
+
+	/**
+	 *
+	 * Similar to {@link #tryToCreateDefaultOAuth2TokenService()} except it tries to create
+	 * {@link XsuaaOAuth2TokenService} and internally depends on spring-web.
+	 *
+	 * @return the {@link XsuaaOAuth2TokenService} or null if it could not be created.
+	 */
+	private OAuth2TokenService tryToCreateXsuaaOAuth2TokenService() {
+		LOGGER.debug("Trying to create XsuaaOAuth2TokenService.");
+		try {
+			return new XsuaaOAuth2TokenService();
+		} catch (Exception | LinkageError e) {
+			LOGGER.debug("Failed to create XsuaaOAuth2TokenService.", e);
+		}
+		return null;
+	}
+
+	// for tests
+	void setOAuth2TokenService(OAuth2TokenService oAuth2TokenService) {
+		this.oAuth2TokenService = oAuth2TokenService;
 	}
 
-	private String[] getMultiValueAttributeFromExtObject(String claimName, String attributeName)
-			throws XSUserInfoException {
+	private String[] getMultiValueAttributeFromExtObject(String claimName, String attributeName) {
 		JsonObject claimAsJsonObject = getClaimAsJsonObject(claimName);
 		return Optional.ofNullable(claimAsJsonObject)
 				.map(jsonObject -> jsonObject.getAsList(attributeName, String.class))
@@ -291,7 +390,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 				.orElseThrow(createXSUserInfoException(attributeName));
 	}
 
-	private void checkNotGrantTypeClientCredentials(String methodName) throws XSUserInfoException {
+	private void checkNotGrantTypeClientCredentials(String methodName) {
 		if (GrantType.CLIENT_CREDENTIALS == accessToken.getGrantType()) {
 			String message = String.format("Method '%s' is not supported for grant type '%s'", methodName,
 					GrantType.CLIENT_CREDENTIALS);
@@ -300,7 +399,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Nullable
-	private String getAttributeFromClaimAsString(String claimName, String attributeName) throws XSUserInfoException {
+	private String getAttributeFromClaimAsString(String claimName, String attributeName) {
 		return Optional.ofNullable(getClaimAsJsonObject(claimName))
 				.map(claim -> claim.getAsString(attributeName)).orElse(null);
 	}
@@ -309,7 +408,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 		return () -> new XSUserInfoException("Invalid user attribute " + attribute);
 	}
 
-	private String getClaimValue(String claimname) throws XSUserInfoException {
+	private String getClaimValue(String claimname) {
 		String value = accessToken.getClaimAsString(claimname);
 		if (value == null) {
 			throw new XSUserInfoException("Invalid user attribute " + claimname);
@@ -318,7 +417,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	@Nullable
-	private JsonObject getClaimAsJsonObject(String claimName) throws XSUserInfoException {
+	private JsonObject getClaimAsJsonObject(String claimName) {
 		try {
 			return accessToken.getClaimAsJsonObject(claimName);
 		} catch (JsonParsingException e) {
@@ -326,8 +425,63 @@ public class XSUserInfoAdapter implements XSUserInfo {
 		}
 	}
 
-	String getExternalAttribute(String attributeName) throws XSUserInfoException {
+	String getExternalAttribute(String attributeName) {
 		return getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, attributeName);
 	}
 
+	private String performTokenFlow(String baseUaaUrl, int tokenRequestType, String clientId, String clientSecret,
+			Map<String, String> additionalAuthAttributes) {
+		try {
+			ClientCredentials clientCredentials = new ClientCredentials(clientId, clientSecret);
+			XsuaaTokenFlows xsuaaTokenFlows = new XsuaaTokenFlows(getOrCreateOAuth2TokenService(),
+					new XsuaaDefaultEndpoints(baseUaaUrl), clientCredentials);
+			return performRequest(xsuaaTokenFlows, tokenRequestType, additionalAuthAttributes);
+		} catch (RuntimeException e) {
+			throw new XSUserInfoException(e.getMessage());
+		}
+	}
+
+	private String performRequest(XsuaaTokenFlows xsuaaTokenFlows, int tokenRequestType,
+			Map<String, String> additionalAuthAttributes) {
+		switch (tokenRequestType) {
+		case XSTokenRequest.TYPE_USER_TOKEN:
+			return performUserTokenFlow(xsuaaTokenFlows, additionalAuthAttributes);
+		case XSTokenRequest.TYPE_CLIENT_CREDENTIALS_TOKEN:
+			return performClientCredentialsFlow(xsuaaTokenFlows, additionalAuthAttributes);
+		default:
+			throw new XSUserInfoException(
+					"Found unsupported XSTokenRequest type. The only supported types are XSTokenRequest.TYPE_USER_TOKEN and XSTokenRequest.TYPE_CLIENT_CREDENTIALS_TOKEN.");
+		}
+	}
+
+	private String performUserTokenFlow(XsuaaTokenFlows xsuaaTokenFlows, Map<String, String> additionalAuthAttributes) {
+		String userToken;
+		try {
+			userToken = xsuaaTokenFlows.userTokenFlow()
+					.subdomain(getSubdomain())
+					.token(getAppToken())
+					.attributes(additionalAuthAttributes)
+					.execute().getAccessToken();
+		} catch (TokenFlowException e) {
+			LOGGER.error("Error performing User Token Flow", e);
+			throw new XSUserInfoException("Error performing User Token Flow.", e);
+		}
+		return userToken;
+	}
+
+	private String performClientCredentialsFlow(XsuaaTokenFlows xsuaaTokenFlows,
+			Map<String, String> additionalAuthAttributes) {
+		String ccfToken;
+		try {
+			ccfToken = xsuaaTokenFlows.clientCredentialsTokenFlow()
+					.subdomain(getSubdomain())
+					.attributes(additionalAuthAttributes)
+					.execute().getAccessToken();
+		} catch (TokenFlowException e) {
+			LOGGER.error("Error performing Client Credentials Flow", e);
+			throw new XSUserInfoException("Error performing Client Credentials Flow..", e);
+		}
+		return ccfToken;
+	}
+
 }
diff --git a/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvironment.java b/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvironment.java
index 5fb406e8..504b77c4 100644
--- a/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvironment.java
+++ b/java-security/src/main/java/com/sap/cloud/security/config/cf/CFEnvironment.java
@@ -22,6 +22,8 @@ import java.util.function.UnaryOperator;
  */
 public class CFEnvironment implements Environment {
 
+	private static final String EMPTY_JSON = "{}";
+
 	private Map<Service, List<OAuth2ServiceConfiguration>> serviceConfigurations;
 	private UnaryOperator<String> systemEnvironmentProvider;
 	private UnaryOperator<String> systemPropertiesProvider;
@@ -90,7 +92,11 @@ public class CFEnvironment implements Environment {
 		if (env == null) {
 			env = systemEnvironmentProvider.apply(VCAP_SERVICES);
 		}
-		return env != null ? env : "{}";
+		return emptyStringOrNull(env) ? EMPTY_JSON : env;
+	}
+
+	private boolean emptyStringOrNull(String env) {
+		return env == null || env.trim().isEmpty();
 	}
 
 	private String extractVcapApplicationJson() {
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java
index f6bc4a38..589c3845 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java
@@ -10,6 +10,7 @@ import org.springframework.util.MultiValueMap;
 import org.springframework.web.client.HttpClientErrorException;
 import org.springframework.web.client.HttpServerErrorException;
 import org.springframework.web.client.RestOperations;
+import org.springframework.web.client.RestTemplate;
 import org.springframework.web.util.UriComponentsBuilder;
 
 import javax.annotation.Nonnull;
@@ -27,11 +28,17 @@ public class XsuaaOAuth2TokenService extends AbstractOAuth2TokenService {
 	private final RestOperations restOperations;
 	private static Logger logger = LoggerFactory.getLogger(XsuaaOAuth2TokenService.class);
 
+	public XsuaaOAuth2TokenService() {
+		this(new RestTemplate());
+	}
+
 	public XsuaaOAuth2TokenService(@Nonnull RestOperations restOperations) {
 		assertNotNull(restOperations, "restOperations is required");
 		this.restOperations = restOperations;
 	}
 
+
+
 	@Override
 	protected OAuth2TokenResponse requestAccessToken(URI tokenEndpointUri, HttpHeaders headers,
 			Map<String, String> parameters) throws OAuth2ServiceException {


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterIntegrationTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterIntegrationTest.java
new file mode 100644
index 00000000..55ecfd57
--- /dev/null
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterIntegrationTest.java
@@ -0,0 +1,418 @@
+package com.sap.cloud.security.adapter.xs;
+
+import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
+import com.sap.cloud.security.config.cf.CFConstants;
+import com.sap.cloud.security.config.cf.CFEnvironment;
+import com.sap.cloud.security.token.XsuaaScopeConverter;
+import com.sap.cloud.security.token.XsuaaToken;
+import com.sap.cloud.security.xsuaa.client.ClientCredentials;
+import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;
+import com.sap.cloud.security.xsuaa.client.OAuth2TokenResponse;
+import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;
+import com.sap.xsa.security.container.XSTokenRequest;
+import com.sap.xsa.security.container.XSUserInfo;
+import com.sap.xsa.security.container.XSUserInfoException;
+import org.apache.commons.io.IOUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.*;
+import org.junit.contrib.java.lang.system.RestoreSystemProperties;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+
+import static com.sap.cloud.security.token.TokenClaims.XSUAA.TRUSTED_CLIENT_ID_SUFFIX;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * This integration test has been ported from the original implementation of
+ * XSUserInfo to ensure compatibility.
+ */
+public class XSUserInfoAdapterIntegrationTest {
+
+	private XSUserInfo infoUser;
+	private XSUserInfo infoUserNoAttr;
+	private XSUserInfo infoCc;
+	private XSUserInfo infoCcNoAttr;
+	private XSUserInfo correctEnduserInfo;
+	private XSUserInfo correctEnduserInfoWithUaaUser;
+	private OAuth2TokenService oAuth2TokenService;
+
+	@Rule
+	public final TestRule restoreSystemProperties = new RestoreSystemProperties();
+
+	@Before
+	public void setup() throws XSUserInfoException, IOException, JSONException {
+		oAuth2TokenService = Mockito.mock(OAuth2TokenService.class);
+		String vcapServices = buildVcapServices("java-hello-world", "default");
+		infoUser = createToken(readData("/token_user.txt"), vcapServices);
+		infoUserNoAttr = createToken(readData("/token_user_noattr.txt"), vcapServices);
+		infoCc = createToken(readData("/token_cc.txt"), vcapServices);
+		infoCcNoAttr = createToken(readData("/token_cc_noattr.txt"), vcapServices);
+		correctEnduserInfo = createToken(readData("/correctEndUserToken.txt"), vcapServices);
+		correctEnduserInfoWithUaaUser = createToken(readData("/correctEndUserTokenUaaUser.txt"), vcapServices);
+	}
+
+	private XSUserInfo createToken(String token, String vcapServices) {
+		System.setProperty(CFConstants.VCAP_SERVICES, vcapServices); // restored by RestoreSystemProperties rule
+		OAuth2ServiceConfiguration xsuaaConfiguration = CFEnvironment.getInstance().getXsuaaConfiguration();
+		String appId = xsuaaConfiguration.getProperty(CFConstants.XSUAA.APP_ID);
+		XsuaaToken accessToken = new XsuaaToken(token).withScopeConverter(new XsuaaScopeConverter(appId));
+		XSUserInfoAdapter xsUserInfoAdapter = new XSUserInfoAdapter(accessToken, xsuaaConfiguration);
+		xsUserInfoAdapter.setOAuth2TokenService(oAuth2TokenService);
+		return xsUserInfoAdapter;
+	}
+
+	@Test
+	public void getLogonName() {
+		assertEquals("WOLFGANG", infoUser.getLogonName());
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getLogonNameCc() {
+		infoCc.getLogonName();
+	}
+
+	@Test
+	public void getEmail() {
+		assertEquals("WOLFGANG@unknown", infoUser.getEmail());
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getEmailCc() {
+		infoCc.getEmail();
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getGivenName() {
+		infoUser.getGivenName(); // not in token
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getGivenNameCc() {
+		infoCc.getGivenName();
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getFamilyName() {
+		infoUser.getFamilyName(); // not in token
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getFamilyNameCc() {
+		infoCc.getFamilyName();
+	}
+
+	@Test
+	public void getOrigin() {
+		assertEquals("useridp", infoUser.getOrigin());
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getOriginCc() {
+		Assert.assertNull(infoCc.getOrigin()); // not in token
+	}
+
+	@Test
+	public void getIdentityZone() {
+		assertEquals("uaa", infoUser.getIdentityZone());
+		assertEquals("uaa", infoUser.getSubaccountId());
+	}
+
+	@Test
+	public void getClientId() {
+		assertEquals("sb-java-hello-world", infoUser.getClientId());
+	}
+
+	@Test
+	public void checkScope() {
+		assertEquals(false, infoUser.checkScope("cloud_controller.read"));
+		assertEquals(true, infoUser.checkScope("java-hello-world.Display"));
+	}
+
+	private JSONObject buildBinding(String xsappname, String planName) throws JSONException {
+		JSONObject credentials = new JSONObject();
+		credentials.put("verificationkey", "key");
+		credentials.put(CFConstants.XSUAA.APP_ID, xsappname);
+		credentials.put(TRUSTED_CLIENT_ID_SUFFIX, xsappname);
+		JSONObject binding = new JSONObject();
+		binding.put("name", planName + "-uaa");
+		binding.put("label", "xsuaa");
+		binding.put("tags", new JSONArray().put("xsuaa"));
+		binding.put("plan", planName);
+		binding.put("credentials", credentials);
+		return binding;
+	}
+
+	private String buildVcapServices(String xsappname, String planName) throws JSONException {
+		JSONObject vcapServices = new JSONObject();
+		JSONArray bindingArray = new JSONArray();
+		bindingArray.put(buildBinding(xsappname, planName));
+		vcapServices.put("xsuaa", bindingArray);
+		return vcapServices.toString();
+	}
+
+	private String buildVcapServices(String xsappname1, String planName1, String xsappname2, String planName2)
+			throws JSONException {
+		JSONObject vcapServices = new JSONObject();
+		JSONArray bindingArray = new JSONArray();
+		bindingArray.put(buildBinding(xsappname1, planName1));
+		bindingArray.put(buildBinding(xsappname2, planName2));
+		vcapServices.put("xsuaa", bindingArray);
+		return vcapServices.toString();
+	}
+
+	@Test
+	public void checkLocalScopeVcapServicesOneBinding() throws XSUserInfoException, IOException, JSONException {
+		// test default plan binding with default plan token
+		String token = readData("/token_user.txt");
+		XSUserInfo userInfo = createToken(token, buildVcapServices("cloud_controller", "default"));
+		Assert.assertFalse(userInfo.checkLocalScope("read"));
+		userInfo = createToken(token, buildVcapServices("java-hello-world", "default"));
+		Assert.assertTrue(userInfo.checkLocalScope("Display"));
+		// test application plan binding with default plan token
+		userInfo = createToken(token, buildVcapServices("cloud_controller", "application"));
+		Assert.assertFalse(userInfo.checkLocalScope("read"));
+		userInfo = createToken(token, buildVcapServices("java-hello-world", "application"));
+		Assert.assertTrue(userInfo.checkLocalScope("Display"));
+	}
+
+	/**
+	 * The method {@link XSUserInfoAdapter#checkLocalScope(String)} does not throw
+	 * the {@link XSUserInfoException} when it still can find a valid configuration
+	 * in the environment. This is a (breaking) change in contrast to the original
+	 * implementation of {@link XSUserInfo}. This test is not applicable but stays
+	 * to document what has been changed in the implementation.
+	 */
+	@Test
+	@Ignore
+	public void checkLocalScopeVcapServicesTwoBindings() throws XSUserInfoException, IOException, JSONException {
+		// test default & application plan binding with default plan token
+		String vcapServices = buildVcapServices("cloud_controller", "default",
+				"java-hello-world!t5", "application");
+		String token = readData("/token_user.txt");
+		XSUserInfo userInfo = createToken(token, vcapServices);
+		Assert.assertFalse(userInfo.checkLocalScope("read"));
+		vcapServices = buildVcapServices("java-hello-world", "default",
+				"java-hello-world!t5", "application");
+		userInfo = createToken(token, vcapServices);
+		Assert.assertTrue(userInfo.checkLocalScope("Display"));
+
+		// test broker & application plan binding with default plan token
+		vcapServices = buildVcapServices("cloud_controller!b4", "broker", "java-hello-world!t5", "application");
+		userInfo = createToken(token, vcapServices);
+		try {
+			Assert.assertFalse(userInfo.checkLocalScope("read"));
+			fail();
+		} catch (XSUserInfoException e) {
+			assertEquals("Property xsappname not found in VCAP_SERVICES, must be declared in xs-security.json",
+					e.getMessage());
+		}
+		vcapServices = buildVcapServices("java-hello-world!b4", "broker", "java-hello-world!t5", "application");
+		userInfo = createToken(token, vcapServices);
+		try {
+			Assert.assertFalse(userInfo.checkLocalScope("Display"));
+			fail();
+		} catch (XSUserInfoException e) {
+			assertEquals("Property xsappname not found in VCAP_SERVICES, must be declared in xs-security.json",
+					e.getMessage());
+		}
+		// test two default plan binding with default plan token
+		vcapServices = buildVcapServices("cloud_controller", "default", "node-hello-world", "default");
+		userInfo = createToken(token, vcapServices);
+		try {
+			Assert.assertFalse(userInfo.checkLocalScope("read"));
+			fail();
+		} catch (XSUserInfoException e) {
+			assertEquals("Property xsappname not found in VCAP_SERVICES, must be declared in xs-security.json",
+					e.getMessage());
+		}
+		vcapServices = buildVcapServices("cloud_controller", "default", "java-hello-world", "default");
+		userInfo = createToken(token, vcapServices);
+		try {
+			Assert.assertFalse(userInfo.checkLocalScope("Display"));
+			fail();
+		} catch (XSUserInfoException e) {
+			assertEquals("Property xsappname not found in VCAP_SERVICES, must be declared in xs-security.json",
+					e.getMessage());
+		}
+		// test two application plan binding with default plan token
+		vcapServices = buildVcapServices("node-hello-world!t5", "application", "java-hello-world!t5", "application");
+		userInfo = createToken(token, vcapServices);
+		try {
+			Assert.assertFalse(userInfo.checkLocalScope("read"));
+			fail();
+		} catch (XSUserInfoException e) {
+			assertEquals("Property xsappname not found in VCAP_SERVICES, must be declared in xs-security.json",
+					e.getMessage());
+		}
+		vcapServices = buildVcapServices("node-hello-world!t5", "application", "java-hello-world!t5", "application");
+		userInfo = createToken(token, vcapServices);
+		try {
+			Assert.assertFalse(userInfo.checkLocalScope("Display"));
+			fail();
+		} catch (XSUserInfoException e) {
+			assertEquals("Property xsappname not found in VCAP_SERVICES, must be declared in xs-security.json",
+					e.getMessage());
+		}
+	}
+
+	@Test
+	public void getAttribute() {
+		String[] cost_center = infoUser.getAttribute("cost center");
+		assertEquals(2, cost_center.length);
+		assertEquals("0815", cost_center[0]);
+		assertEquals("4711", cost_center[1]);
+		String[] country = infoUser.getAttribute("country");
+		assertEquals(1, country.length);
+		assertEquals("Germany", country[0]);
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getAttributeNotExisting() {
+		infoUserNoAttr.getAttribute("cost center");
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getAttributeCc() {
+		infoCc.getAttribute("cost center");
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void getAttributeCcNotExisting() {
+		infoCcNoAttr.getAttribute("cost center");
+	}
+
+	@Test
+	public void hasAttributes() {
+		Assert.assertTrue(infoUser.hasAttributes());
+		Assert.assertTrue(!infoUserNoAttr.hasAttributes());
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void hasAttributesCc() {
+		infoCc.hasAttributes();
+	}
+
+	@Test
+	public void testServiceInstanceId() {
+		assertEquals("abcd1234", infoUser.getCloneServiceInstanceId());
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void testFailServiceInstanceIdNoId() {
+		assertEquals("abcd1234", infoUserNoAttr.getCloneServiceInstanceId());
+	}
+
+	@Test
+	public void testAdditionalAuthAttr() {
+		assertEquals("abcd1234", infoUser.getAdditionalAuthAttribute("external_id"));
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void testFailAdditionalAuthAttrNoAttr() {
+		assertEquals("abcd1234", infoUserNoAttr.getAdditionalAuthAttribute("external_id"));
+	}
+
+	@Test
+	public void getToken() {
+		Assert.assertNotNull("Token must not be null", infoUser.getHdbToken());
+		Assert.assertTrue(!infoUser.getHdbToken().isEmpty());
+		Assert.assertTrue(infoUser.getHdbToken().equals(infoUser.getToken("SYSTEM", "HDB")));
+		Assert.assertTrue(!infoUser.getAppToken().isEmpty());
+		Assert.assertTrue(infoUser.getAppToken().equals(infoUser.getToken("SYSTEM", "JobScheduler")));
+	}
+
+	@Test
+	public void getTokenCc() {
+		Assert.assertNotNull("Token must not be null", infoCc.getHdbToken());
+		Assert.assertTrue(!infoCc.getHdbToken().isEmpty());
+		Assert.assertTrue(infoCc.getHdbToken().equals(infoCc.getToken("SYSTEM", "HDB")));
+		Assert.assertTrue(!infoCc.getAppToken().isEmpty());
+		Assert.assertTrue(infoCc.getAppToken().equals(infoCc.getToken("SYSTEM", "JobScheduler")));
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void requestTokenForClientTestInvalidClientId() {
+		correctEnduserInfoWithUaaUser.requestTokenForClient(null, "foo", "bar");
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void requestTokenForClientTestInvalidClientSecret() {
+		correctEnduserInfoWithUaaUser.requestTokenForClient("foo", null, "bar");
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void requestTokenForClientTestInvalidUaaUrl() {
+		correctEnduserInfoWithUaaUser.requestTokenForClient("foo", "bar", null);
+	}
+
+	private String readData(String path) throws IOException {
+		return IOUtils.resourceToString("/userInfoIntegration" + path, StandardCharsets.UTF_8);
+	}
+
+	@Rule
+	public ExpectedException userInfoException = ExpectedException.none();
+
+	@Test
+	public void jwtBearerFlowSuccess() throws Exception {
+		// prepare mocks
+		String testToken = "mytesttoken";
+		XSTokenRequest request = createXSTokenRequest();
+
+		OAuth2TokenResponse oAuth2TokenResponse = mock(OAuth2TokenResponse.class);
+		ClientCredentials clientCredentials = new ClientCredentials(request.getClientId(), request.getClientSecret());
+		URI tokenEndpointUri = URI.create(request.getTokenEndpoint() + "/oauth/token");
+		when(oAuth2TokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
+				eq(tokenEndpointUri),
+				eq(clientCredentials),
+				eq(correctEnduserInfo.getAppToken()),
+				any(),
+				anyMap())).thenReturn(oAuth2TokenResponse);
+		when(oAuth2TokenResponse.getAccessToken()).thenReturn(testToken);
+
+		// execute flow
+		String token = correctEnduserInfo.requestToken(request);
+
+		// verify token
+		assertEquals(testToken, token);
+	}
+
+	@Test(expected = XSUserInfoException.class)
+	public void jwtBearerFlowAuthFail() throws Exception {
+		// prepare mocks
+		XSTokenRequest request = createXSTokenRequest();
+		ClientCredentials clientCredentials = new ClientCredentials(request.getClientId(), request.getClientSecret());
+		URI tokenEndpointUri = URI.create(request.getTokenEndpoint() + "/oauth/token");
+
+		when(oAuth2TokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
+				eq(tokenEndpointUri),
+				eq(clientCredentials),
+				eq(correctEnduserInfo.getAppToken()),
+				any(),
+				anyMap())).thenThrow(OAuth2ServiceException
+						.createWithStatusCodeAndResponseBody("Unauthorized", org.apache.http.HttpStatus.SC_UNAUTHORIZED,
+								"Unauthorized"));
+		// execute flow
+		correctEnduserInfo.requestToken(request);
+	}
+
+	private XSTokenRequest createXSTokenRequest() {
+		XSTokenRequest request = mock(XSTokenRequest.class);
+		when(request.getTokenEndpoint()).thenReturn(URI.create("http://localhost:8080"));
+		when(request.getClientId()).thenReturn("test");
+		when(request.getClientSecret()).thenReturn("secret");
+		when(request.getType()).thenReturn(XSTokenRequest.TYPE_USER_TOKEN);
+		when(request.isValid()).thenReturn(true);
+		return request;
+	}
+}
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 48e7c915..73d2b63a 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
@@ -9,6 +9,7 @@ import com.sap.cloud.security.token.*;
 import com.sap.xsa.security.container.XSUserInfoException;
 import org.apache.commons.io.IOUtils;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.mockito.Mockito;
 
@@ -46,8 +47,8 @@ public class XSUserInfoAdapterTest {
 
 	@Test
 	public void constructors() throws XSUserInfoException {
-		assertThat(new XSUserInfoAdapter((Object)token).getLogonName()).isEqualTo("TestUser");
-		assertThat(new XSUserInfoAdapter((Token)token).getLogonName()).isEqualTo("TestUser");
+		assertThat(new XSUserInfoAdapter((Object) token).getLogonName()).isEqualTo("TestUser");
+		assertThat(new XSUserInfoAdapter((Token) token).getLogonName()).isEqualTo("TestUser");
 		assertThat(new XSUserInfoAdapter((AccessToken) token).getLogonName()).isEqualTo("TestUser");
 		assertThatThrownBy(() -> new XSUserInfoAdapter(null)).isInstanceOf(XSUserInfoException.class)
 				.hasMessageContaining("null")
@@ -57,7 +58,6 @@ public class XSUserInfoAdapterTest {
 				.hasMessageContaining("instance of AccessToken");
 	}
 
-	// TODO: implement external context fallback tests
 	@Test
 	public void testGetLogonName() throws XSUserInfoException {
 		assertThat(cut.getLogonName()).isEqualTo("TestUser");
@@ -307,20 +307,6 @@ public class XSUserInfoAdapterTest {
 		assertThatThrownBy(() -> cut.getSystemAttribute(nonExistingAttribute)).isInstanceOf(XSUserInfoException.class);
 	}
 
-	@Test
-	public void testRequestTokenForClient_isNotImplemented() {
-		assertThatThrownBy(() -> cut.requestTokenForClient("", "", ""))
-				.isInstanceOf(UnsupportedOperationException.class)
-				.hasMessageContaining("Not implemented");
-	}
-
-	@Test
-	public void testRequestToken_isNotImplemented() {
-		assertThatThrownBy(() -> cut.requestToken(null))
-				.isInstanceOf(UnsupportedOperationException.class)
-				.hasMessageContaining("Not implemented");
-	}
-
 	@Test
 	public void getHdbToken_AuthCodeToken_NoAttributes() throws XSUserInfoException, IOException {
 		XsuaaToken token = new XsuaaToken(
@@ -403,6 +389,23 @@ public class XSUserInfoAdapterTest {
 		assertThat(cut.isInForeignMode()).isFalse();
 	}
 
+	@Test
+	public void isForeignModeFalse_WhenIdentityZoneDoesNotMatchButCliendIdIsBrokerPlan()
+			throws XSUserInfoException {
+		String tokenClientId = "sb-application!b0123"; // cid
+		String identityZone = "brokerplanmasterapp!b123"; // ext_attr -> zdn
+		OAuth2ServiceConfiguration configuration = OAuth2ServiceConfigurationBuilder.forService(Service.XSUAA)
+				.withClientId(tokenClientId)
+				.withProperty(IDENTITY_ZONE, identityZone)
+				.build();
+
+		cut = createComponentUnderTestSpy(configuration);
+		doReturn(tokenClientId).when(cut).getClientId();
+		doReturn("otherIdentityZone").when(cut).getIdentityZone();
+
+		assertThat(cut.isInForeignMode()).isFalse();
+	}
+
 	@Test
 	public void isForeignModeFalse_WhenClientIdAndIdentityZonesMatch() throws XSUserInfoException {
 		String tokenClientId = "sb-application"; // cid
@@ -458,7 +461,8 @@ public class XSUserInfoAdapterTest {
 
 	private XSUserInfoAdapter createComponentUnderTestSpy(OAuth2ServiceConfiguration configuration)
 			throws XSUserInfoException {
-		return spy(new XSUserInfoAdapter(Mockito.mock(XsuaaToken.class), configuration));
+		return spy(
+				new XSUserInfoAdapter(Mockito.mock(XsuaaToken.class), configuration));
 	}
 
 	private XsuaaToken createMockToken(GrantType grantType) {
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 f12971ea..b7ea66c8 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
@@ -202,4 +202,28 @@ public class CFEnvironmentTest {
 		OAuth2ServiceConfiguration config = cut.getXsuaaConfiguration();
 		assertThat(Plan.from(config.getProperty(SERVICE_PLAN))).isEqualTo(Plan.SPACE);
 	}
+
+	@Test
+	public void getXsuaaConfiguration_noVcapServices_doesNotThrowExceptions() {
+		cut = CFEnvironment.getInstance((any) -> null, (any) -> null);
+
+		assertThat(cut.getXsuaaConfiguration()).isNull();
+		assertThat(cut.getNumberOfXsuaaConfigurations()).isZero();
+	}
+
+	@Test
+	public void getXsuaaConfiguration_vcapServicesEmptyString_doesNotThrowExceptions() {
+		cut = CFEnvironment.getInstance((any) -> "", (any) -> "");
+
+		assertThat(cut.getXsuaaConfiguration()).isNull();
+		assertThat(cut.getNumberOfXsuaaConfigurations()).isZero();
+	}
+
+	@Test
+	public void getXsuaaConfiguration_vcapServicesEmptyJson_doesNotThrowExceptions() {
+		cut = CFEnvironment.getInstance((any) -> "{}", (any) -> "{}");
+
+		assertThat(cut.getXsuaaConfiguration()).isNull();
+		assertThat(cut.getNumberOfXsuaaConfigurations()).isZero();
+	}
 }
\ No newline at end of file
diff --git a/java-security/src/test/resources/userInfoIntegration/correctEndUserToken.txt b/java-security/src/test/resources/userInfoIntegration/correctEndUserToken.txt
new file mode 100644
index 00000000..a5322b52
--- /dev/null
+++ b/java-security/src/test/resources/userInfoIntegration/correctEndUserToken.txt
@@ -0,0 +1,1 @@
+eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjNjgzMTEyNS0xZWQ2LTQxYjAtOGVhOC1lNjBhMzQxYTI3ODciLCJzdWIiOiI0MjUxMzAiLCJzY29wZSI6WyJvcGVuaWQiLCJ1YWEucmVzb3VyY2UiXSwiY2xpZW50X2lkIjoic2IteHNzZWN0ZXN0IiwiY2lkIjoic2IteHNzZWN0ZXN0IiwiYXpwIjoic2IteHNzZWN0ZXN0IiwiZ3JhbnRfdHlwZSI6InBhc3N3b3JkIiwidXNlcl9pZCI6IjQyNTEzMCIsInVzZXJfbmFtZSI6Ik5PREVURVNUVVNFUiIsImVtYWlsIjoiTm9kZXRlc3RAc2FwLmNvbSIsImdpdmVuX25hbWUiOiJOb2RldGVzdEZpcnN0TmFtZSIsImZhbWlseV9uYW1lIjoiTm9kZXRlc3RMYXN0TmFtZSIsImlhdCI6MTQ3MDgxNTQzNCwiZXhwIjoyMTAxNTM1NDM0LCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidGVzdC1pZHoiLCJoZGIubmFtZWR1c2VyLnNhbWwiOiI8P3htbCB2ZXJzaW9uPVwiMS4wXCIgZW5jb2Rpbmc9XCJVVEYtOFwiPz48c2FtbDI6QXNzZXJ0aW9uIHhtbG5zOnNhbWwyPVwidXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvblwiIElEPVwiXzcxZWUxNzc2LTlkMmYtNDk3My1hY2E4LTllMjJiMjk2N2FjOFwiIElzc3VlSW5zdGFudD1cIjIwMTYtMDgtMTBUMDc6NDU6MzQuMzQ3WlwiIFZlcnNpb249XCIyLjBcIj48c2FtbDI6SXNzdWVyPlRTVC1zYW1sPC9zYW1sMjpJc3N1ZXI-PGRzOlNpZ25hdHVyZSB4bWxuczpkcz1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNcIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biNcIi8-PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTFcIi8-PGRzOlJlZmVyZW5jZSBVUkk9XCIjXzcxZWUxNzc2LTlkMmYtNDk3My1hY2E4LTllMjJiMjk2N2FjOFwiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmVcIi8-PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biNcIi8-PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTFcIi8-PGRzOkRpZ2VzdFZhbHVlPm91OHIzUjBXQkhHMWJwNEtLT3gxUHlWT2lZQT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU-TGJSS3Yxci9oN0lNbWlTeXgxMFdrTTdKdWVrcm13eVZOc0I1M3BrRlJucmpDR1d0bUZrUXNrbnNMN2VUVU40K2djSkdXMHFHVFVtdlVrZlhFMU84cmYyQ21UY0MwMWNZc0dBWldiTnBPTE5tcFA5Z0c2NTcycHZlUnFqVFhMR1NpbE0yZWpKaXlscTJKbkZMaFhwZ3JuVGJDdlFXNmE5SlRwUnB2TXo4U2lTb2R4YXg3ckp3N0MweVp6VXE4NjJNNXlOamRvSUhoRWtuZ01jQzVMRERoZnBmNlRrUU1zeVZjTWFtRHFqVFM3V1RndmtRS2w1cGtPUEtFdWhUakNSN1A3S0Fla2VEbVlvcXM3eUVacnJkS0VpeFNZNGk1RjN3ZU0rZHcrQTF1ZTlqRjJLbWVSdmpveHMyaHdmc1d3VXZDeHkrMkpocjU0dmF0bXdlRzhkSTBRPT08L2RzOlNpZ25hdHVyZVZhbHVlPjwvZHM6U2lnbmF0dXJlPjxzYW1sMjpTdWJqZWN0PjxzYW1sMjpOYW1lSUQgRm9ybWF0PVwidXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6dW5zcGVjaWZpZWRcIj5OT0RFVEVTVFVTRVI8L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyXCI-PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj1cIjIwMTYtMDgtMTBUMTE6NTA6MzQuMzQ3WlwiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24-PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT1cIjIwMTYtMDgtMTBUMDc6NDU6MzQuMzQ3WlwiIE5vdE9uT3JBZnRlcj1cIjIwMTYtMDgtMTBUMTE6NTA6MzQuMzQ3WlwiLz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PVwiMjAxNi0wOC0xMFQwNzo1MDozNC4zNDdaXCIgU2Vzc2lvbk5vdE9uT3JBZnRlcj1cIjIwMTYtMDgtMTBUMDc6NTU6MzQuMzQ3WlwiPjxzYW1sMjpBdXRobkNvbnRleHQ-PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPiIsImF1ZCI6WyJzYi14c3NlY3Rlc3QiLCJvcGVuaWQiXX0.vVG-iWew7lyNHa-07e9cWbqXNYFbXjzuzG3VwVTkEUE
\ No newline at end of file
diff --git a/java-security/src/test/resources/userInfoIntegration/correctEndUserTokenUaaUser.txt b/java-security/src/test/resources/userInfoIntegration/correctEndUserTokenUaaUser.txt
new file mode 100644
index 00000000..676242fa
--- /dev/null
+++ b/java-security/src/test/resources/userInfoIntegration/correctEndUserTokenUaaUser.txt
@@ -0,0 +1,1 @@
+eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJjNjgzMTEyNS0xZWQ2LTQxYjAtOGVhOC1lNjBhMzQxYTI3ODciLCJzdWIiOiI0MjUxMzAiLCJzY29wZSI6WyJvcGVuaWQiLCJ1YWEudXNlciIsInVhYS5yZXNvdXJjZSJdLCJjbGllbnRfaWQiOiJzYi14c3NlY3Rlc3QiLCJjaWQiOiJzYi14c3NlY3Rlc3QiLCJhenAiOiJzYi14c3NlY3Rlc3QiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX2lkIjoiNDI1MTMwIiwidXNlcl9uYW1lIjoiTk9ERVRFU1RVU0VSIiwiZW1haWwiOiJOb2RldGVzdEBzYXAuY29tIiwiZ2l2ZW5fbmFtZSI6Ik5vZGV0ZXN0Rmlyc3ROYW1lIiwiZmFtaWx5X25hbWUiOiJOb2RldGVzdExhc3ROYW1lIiwiaWF0IjoxNDcwODE1NDM0LCJleHAiOjIxMDE1MzU0MzQsImlzcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC91YWEvb2F1dGgvdG9rZW4iLCJ6aWQiOiJ0ZXN0LWlkeiIsImhkYi5uYW1lZHVzZXIuc2FtbCI6Ijw_eG1sIHZlcnNpb249XCIxLjBcIiBlbmNvZGluZz1cIlVURi04XCI_PjxzYW1sMjpBc3NlcnRpb24geG1sbnM6c2FtbDI9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uXCIgSUQ9XCJfNzFlZTE3NzYtOWQyZi00OTczLWFjYTgtOWUyMmIyOTY3YWM4XCIgSXNzdWVJbnN0YW50PVwiMjAxNi0wOC0xMFQwNzo0NTozNC4zNDdaXCIgVmVyc2lvbj1cIjIuMFwiPjxzYW1sMjpJc3N1ZXI-VFNULXNhbWw8L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI1wiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMVwiLz48ZHM6UmVmZXJlbmNlIFVSST1cIiNfNzFlZTE3NzYtOWQyZi00OTczLWFjYTgtOWUyMmIyOTY3YWM4XCI-PGRzOlRyYW5zZm9ybXM-PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZVwiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48L2RzOlRyYW5zZm9ybXM-PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMVwiLz48ZHM6RGlnZXN0VmFsdWU-b3U4cjNSMFdCSEcxYnA0S0tPeDFQeVZPaVlBPTwvZHM6RGlnZXN0VmFsdWU-PC9kczpSZWZlcmVuY2U-PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5MYlJLdjFyL2g3SU1taVN5eDEwV2tNN0p1ZWtybXd5Vk5zQjUzcGtGUm5yakNHV3RtRmtRc2tuc0w3ZVRVTjQrZ2NKR1cwcUdUVW12VWtmWEUxTzhyZjJDbVRjQzAxY1lzR0FaV2JOcE9MTm1wUDlnRzY1NzJwdmVScWpUWExHU2lsTTJlakppeWxxMkpuRkxoWHBncm5UYkN2UVc2YTlKVHBScHZNejhTaVNvZHhheDdySnc3QzB5WnpVcTg2Mk01eU5qZG9JSGhFa25nTWNDNUxERGhmcGY2VGtRTXN5VmNNYW1EcWpUUzdXVGd2a1FLbDVwa09QS0V1aFRqQ1I3UDdLQWVrZURtWW9xczd5RVpycmRLRWl4U1k0aTVGM3dlTStkdytBMXVlOWpGMkttZVJ2am94czJod2ZzV3dVdkN4eSsySmhyNTR2YXRtd2VHOGRJMFE9PTwvZHM6U2lnbmF0dXJlVmFsdWU-PC9kczpTaWduYXR1cmU-PHNhbWwyOlN1YmplY3Q-PHNhbWwyOk5hbWVJRCBGb3JtYXQ9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZFwiPk5PREVURVNUVVNFUjwvc2FtbDI6TmFtZUlEPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD1cInVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXJcIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPVwiMjAxNi0wOC0xMFQxMTo1MDozNC4zNDdaXCIvPjwvc2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWwyOlN1YmplY3Q-PHNhbWwyOkNvbmRpdGlvbnMgTm90QmVmb3JlPVwiMjAxNi0wOC0xMFQwNzo0NTozNC4zNDdaXCIgTm90T25PckFmdGVyPVwiMjAxNi0wOC0xMFQxMTo1MDozNC4zNDdaXCIvPjxzYW1sMjpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9XCIyMDE2LTA4LTEwVDA3OjUwOjM0LjM0N1pcIiBTZXNzaW9uTm90T25PckFmdGVyPVwiMjAxNi0wOC0xMFQwNzo1NTozNC4zNDdaXCI-PHNhbWwyOkF1dGhuQ29udGV4dD48c2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY-dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDI6QXV0aG5Db250ZXh0Pjwvc2FtbDI6QXV0aG5TdGF0ZW1lbnQ-PC9zYW1sMjpBc3NlcnRpb24-IiwiYXVkIjpbInNiLXhzc2VjdGVzdCIsIm9wZW5pZCJdfQ.GjDBrmEt5ict37lh8flI6TfuTpfW8o7JiVAlWiMC2kw
\ No newline at end of file
diff --git a/java-security/src/test/resources/userInfoIntegration/token_cc.txt b/java-security/src/test/resources/userInfoIntegration/token_cc.txt
new file mode 100644
index 00000000..4fd0f43d
--- /dev/null
+++ b/java-security/src/test/resources/userInfoIntegration/token_cc.txt
@@ -0,0 +1,1 @@
+eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ZTdiM2IwMC03NzUxLTRiNDYtOWIxYS01YTQ2YTJhMWQ1YjgiLCJ4cy51c2VyLmF0dHJpYnV0ZXMiOnsiY29zdCBjZW50ZXIiOlsiMDgxNSIsIjQ3MTEiXSwiY291bnRyeSI6WyJHZXJtYW55Il19LCJzdWIiOiIxMDAyMTkxIiwic2NvcGUiOlsiamF2YS1oZWxsby13b3JsZC5EaXNwbGF5Iiwib3BlbmlkIiwiamF2YS1oZWxsby13b3JsZC5EZWxldGUiLCJqYXZhLWhlbGxvLXdvcmxkLkNyZWF0ZSJdLCJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJncmFudF90eXBlIjoiY2xpZW50X2NyZWRlbnRpYWxzIiwidXNlcl9pZCI6IjEwMDIxOTEiLCJ1c2VyX25hbWUiOiJXT0xGR0FORyIsImVtYWlsIjoiV09MRkdBTkdAdW5rbm93biIsImlhdCI6MTQ0MjkxMjI0NCwiZXhwIjoxNDQyOTU1MzIyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiaGRiLm5hbWVkdXNlci5zYW1sIjoiPD94bWwgdmVyc2lvbj1cIjEuMFwiIGVuY29kaW5nPVwiVVRGLThcIj8-PHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj1cInVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb25cIiBJRD1cIl9mOTQ2YmM3Yi05MzM5LTQyMDAtYjdlYi0xYmJhNjU4MDEwNmJcIiBJc3N1ZUluc3RhbnQ9XCIyMDE1LTA5LTIyVDA4OjU1OjIyLjc1NVpcIiBWZXJzaW9uPVwiMi4wXCI-PHNhbWwyOklzc3Vlcj54czItbG9naW4tdGVzdC5jZi5zYXAtY2YuY29tLXNhbWwtbG9naW48L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI1wiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMVwiLz48ZHM6UmVmZXJlbmNlIFVSST1cIiNfZjk0NmJjN2ItOTMzOS00MjAwLWI3ZWItMWJiYTY1ODAxMDZiXCI-PGRzOlRyYW5zZm9ybXM-PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZVwiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48L2RzOlRyYW5zZm9ybXM-PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMVwiLz48ZHM6RGlnZXN0VmFsdWU-bkNCRk5hUjR5ZlhIYUlDdHZaaDZUVnBYbUl3PTwvZHM6RGlnZXN0VmFsdWU-PC9kczpSZWZlcmVuY2U-PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5DVkp2SVBnZk9ZNnZtRjg5VGZtcHhZbUlWQWFRM2JtSlRhamVOTnVNRUMxWE50SndPenoyT2VGNnhWVjFvWkk1MzZLQURNeVBmTlZwRG5tdVBaQUVzNnFJRzdLVkZlWCtQMTNHUVgyMzdobURxeDFVZHRuTTk0Q2E4eVd5YnRhSXhjWStPMjBQYkFFa1RaQVpyUCtyczBtTjc4QUlGTk1KYStzSVpOYm5MdVU9PC9kczpTaWduYXR1cmVWYWx1ZT48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEIEZvcm1hdD1cInVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkXCI-V09MRkdBTkc8L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyXCI-PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMTI6NTU6MjIuNzU1WlwiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24-PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT1cIjIwMTUtMDktMjJUMDg6NTU6MjIuNzU1WlwiIE5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMTI6NTU6MjIuNzU1WlwiLz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PVwiMjAxNS0wOS0yMlQwODo1NToyMi43NTdaXCIgU2Vzc2lvbk5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMDk6MDA6MjIuNzU3WlwiPjxzYW1sMjpBdXRobkNvbnRleHQ-PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPiIsImF1ZCI6WyJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiamF2YS1oZWxsby13b3JsZCIsIm9wZW5pZCJdLCJhel9hdHRyIjp7ImV4dGVybmFsX2dyb3VwIjoiZG9tYWluXFxncm91cDEiLCJleHRlcm5hbF9pZCI6ImFiY2QxMjM0In0sImV4dF9hdHRyIjp7InNlcnZpY2VpbnN0YW5jZWlkIjoiYWJjZDEyMzQifX0.YDVwukfuEsIztMGRNXzamo3kEBbbZzK6vAFw_W6k92Q
\ No newline at end of file
diff --git a/java-security/src/test/resources/userInfoIntegration/token_cc_noattr.txt b/java-security/src/test/resources/userInfoIntegration/token_cc_noattr.txt
new file mode 100644
index 00000000..e75c2c40
--- /dev/null
+++ b/java-security/src/test/resources/userInfoIntegration/token_cc_noattr.txt
@@ -0,0 +1,1 @@
+eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ZTdiM2IwMC03NzUxLTRiNDYtOWIxYS01YTQ2YTJhMWQ1YjgiLCJzdWIiOiIxMDAyMTkxIiwic2NvcGUiOlsiamF2YS1oZWxsby13b3JsZC5EaXNwbGF5Iiwib3BlbmlkIiwiamF2YS1oZWxsby13b3JsZC5EZWxldGUiLCJqYXZhLWhlbGxvLXdvcmxkLkNyZWF0ZSJdLCJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJncmFudF90eXBlIjoiY2xpZW50X2NyZWRlbnRpYWxzIiwidXNlcl9pZCI6IjEwMDIxOTEiLCJ1c2VyX25hbWUiOiJXT0xGR0FORyIsImVtYWlsIjoiV09MRkdBTkdAdW5rbm93biIsImlhdCI6MTQ0MjkxMjI0NCwiZXhwIjoxNDQyOTU1MzIyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiaGRiLm5hbWVkdXNlci5zYW1sIjoiPD94bWwgdmVyc2lvbj1cIjEuMFwiIGVuY29kaW5nPVwiVVRGLThcIj8-PHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj1cInVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb25cIiBJRD1cIl9mOTQ2YmM3Yi05MzM5LTQyMDAtYjdlYi0xYmJhNjU4MDEwNmJcIiBJc3N1ZUluc3RhbnQ9XCIyMDE1LTA5LTIyVDA4OjU1OjIyLjc1NVpcIiBWZXJzaW9uPVwiMi4wXCI-PHNhbWwyOklzc3Vlcj54czItbG9naW4tdGVzdC5jZi5zYXAtY2YuY29tLXNhbWwtbG9naW48L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI1wiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMVwiLz48ZHM6UmVmZXJlbmNlIFVSST1cIiNfZjk0NmJjN2ItOTMzOS00MjAwLWI3ZWItMWJiYTY1ODAxMDZiXCI-PGRzOlRyYW5zZm9ybXM-PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZVwiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48L2RzOlRyYW5zZm9ybXM-PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMVwiLz48ZHM6RGlnZXN0VmFsdWU-bkNCRk5hUjR5ZlhIYUlDdHZaaDZUVnBYbUl3PTwvZHM6RGlnZXN0VmFsdWU-PC9kczpSZWZlcmVuY2U-PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5DVkp2SVBnZk9ZNnZtRjg5VGZtcHhZbUlWQWFRM2JtSlRhamVOTnVNRUMxWE50SndPenoyT2VGNnhWVjFvWkk1MzZLQURNeVBmTlZwRG5tdVBaQUVzNnFJRzdLVkZlWCtQMTNHUVgyMzdobURxeDFVZHRuTTk0Q2E4eVd5YnRhSXhjWStPMjBQYkFFa1RaQVpyUCtyczBtTjc4QUlGTk1KYStzSVpOYm5MdVU9PC9kczpTaWduYXR1cmVWYWx1ZT48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEIEZvcm1hdD1cInVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkXCI-V09MRkdBTkc8L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyXCI-PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMTI6NTU6MjIuNzU1WlwiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24-PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT1cIjIwMTUtMDktMjJUMDg6NTU6MjIuNzU1WlwiIE5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMTI6NTU6MjIuNzU1WlwiLz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PVwiMjAxNS0wOS0yMlQwODo1NToyMi43NTdaXCIgU2Vzc2lvbk5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMDk6MDA6MjIuNzU3WlwiPjxzYW1sMjpBdXRobkNvbnRleHQ-PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPiIsImF1ZCI6WyJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiamF2YS1oZWxsby13b3JsZCIsIm9wZW5pZCJdfQ.QLecUeVESc1_yoj-pWZxiS3khH0S0NbKy25jzojYqy4
\ No newline at end of file
diff --git a/java-security/src/test/resources/userInfoIntegration/token_user.txt b/java-security/src/test/resources/userInfoIntegration/token_user.txt
new file mode 100644
index 00000000..115bacb1
--- /dev/null
+++ b/java-security/src/test/resources/userInfoIntegration/token_user.txt
@@ -0,0 +1,1 @@
+eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ZTdiM2IwMC03NzUxLTRiNDYtOWIxYS01YTQ2YTJhMWQ1YjgiLCJ4cy51c2VyLmF0dHJpYnV0ZXMiOnsiY29zdCBjZW50ZXIiOlsiMDgxNSIsIjQ3MTEiXSwiY291bnRyeSI6WyJHZXJtYW55Il19LCJzdWIiOiIxMDAyMTkxIiwic2NvcGUiOlsiamF2YS1oZWxsby13b3JsZC5EaXNwbGF5Iiwib3BlbmlkIiwiamF2YS1oZWxsby13b3JsZC5EZWxldGUiLCJqYXZhLWhlbGxvLXdvcmxkLkNyZWF0ZSJdLCJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6IjEwMDIxOTEiLCJ1c2VyX25hbWUiOiJXT0xGR0FORyIsIm9yaWdpbiI6InVzZXJpZHAiLCJlbWFpbCI6IldPTEZHQU5HQHVua25vd24iLCJpYXQiOjE0NDI5MTIyNDQsImV4cCI6MTQ0Mjk1NTMyMiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3VhYS9vYXV0aC90b2tlbiIsInppZCI6InVhYSIsImhkYi5uYW1lZHVzZXIuc2FtbCI6Ijw_eG1sIHZlcnNpb249XCIxLjBcIiBlbmNvZGluZz1cIlVURi04XCI_PjxzYW1sMjpBc3NlcnRpb24geG1sbnM6c2FtbDI9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uXCIgSUQ9XCJfZjk0NmJjN2ItOTMzOS00MjAwLWI3ZWItMWJiYTY1ODAxMDZiXCIgSXNzdWVJbnN0YW50PVwiMjAxNS0wOS0yMlQwODo1NToyMi43NTVaXCIgVmVyc2lvbj1cIjIuMFwiPjxzYW1sMjpJc3N1ZXI-eHMyLWxvZ2luLXRlc3QuY2Yuc2FwLWNmLmNvbS1zYW1sLWxvZ2luPC9zYW1sMjpJc3N1ZXI-PGRzOlNpZ25hdHVyZSB4bWxuczpkcz1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNcIj48ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biNcIi8-PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTFcIi8-PGRzOlJlZmVyZW5jZSBVUkk9XCIjX2Y5NDZiYzdiLTkzMzktNDIwMC1iN2ViLTFiYmE2NTgwMTA2YlwiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmVcIi8-PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biNcIi8-PC9kczpUcmFuc2Zvcm1zPjxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTFcIi8-PGRzOkRpZ2VzdFZhbHVlPm5DQkZOYVI0eWZYSGFJQ3R2Wmg2VFZwWG1Jdz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU-Q1ZKdklQZ2ZPWTZ2bUY4OVRmbXB4WW1JVkFhUTNibUpUYWplTk51TUVDMVhOdEp3T3p6Mk9lRjZ4VlYxb1pJNTM2S0FETXlQZk5WcERubXVQWkFFczZxSUc3S1ZGZVgrUDEzR1FYMjM3aG1EcXgxVWR0bk05NENhOHlXeWJ0YUl4Y1krTzIwUGJBRWtUWkFaclArcnMwbU43OEFJRk5NSmErc0laTmJuTHVVPTwvZHM6U2lnbmF0dXJlVmFsdWU-PC9kczpTaWduYXR1cmU-PHNhbWwyOlN1YmplY3Q-PHNhbWwyOk5hbWVJRCBGb3JtYXQ9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDp1bnNwZWNpZmllZFwiPldPTEZHQU5HPC9zYW1sMjpOYW1lSUQ-PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPVwidXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlclwiPjxzYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9XCIyMDE1LTA5LTIyVDEyOjU1OjIyLjc1NVpcIi8-PC9zYW1sMjpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDI6U3ViamVjdD48c2FtbDI6Q29uZGl0aW9ucyBOb3RCZWZvcmU9XCIyMDE1LTA5LTIyVDA4OjU1OjIyLjc1NVpcIiBOb3RPbk9yQWZ0ZXI9XCIyMDE1LTA5LTIyVDEyOjU1OjIyLjc1NVpcIi8-PHNhbWwyOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD1cIjIwMTUtMDktMjJUMDg6NTU6MjIuNzU3WlwiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9XCIyMDE1LTA5LTIyVDA5OjAwOjIyLjc1N1pcIj48c2FtbDI6QXV0aG5Db250ZXh0PjxzYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY-PC9zYW1sMjpBdXRobkNvbnRleHQ-PC9zYW1sMjpBdXRoblN0YXRlbWVudD48L3NhbWwyOkFzc2VydGlvbj4iLCJhdWQiOlsic2ItamF2YS1oZWxsby13b3JsZCIsImphdmEtaGVsbG8td29ybGQiLCJvcGVuaWQiXSwiYXpfYXR0ciI6eyJleHRlcm5hbF9ncm91cCI6ImRvbWFpblxcZ3JvdXAxIiwiZXh0ZXJuYWxfaWQiOiJhYmNkMTIzNCJ9LCJleHRfYXR0ciI6eyJzZXJ2aWNlaW5zdGFuY2VpZCI6ImFiY2QxMjM0IiwiemRuIjoidGVzdHN1YmRvbWFpbiJ9fQ.Rh7-A4Cx0o_wsIrrTz2xOSHEVLJQMqtjPxD1u_YzRww
\ No newline at end of file
diff --git a/java-security/src/test/resources/userInfoIntegration/token_user_noattr.txt b/java-security/src/test/resources/userInfoIntegration/token_user_noattr.txt
new file mode 100644
index 00000000..de311aa1
--- /dev/null
+++ b/java-security/src/test/resources/userInfoIntegration/token_user_noattr.txt
@@ -0,0 +1,1 @@
+eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ZTdiM2IwMC03NzUxLTRiNDYtOWIxYS01YTQ2YTJhMWQ1YjgiLCJzdWIiOiIxMDAyMTkxIiwic2NvcGUiOlsiamF2YS1oZWxsby13b3JsZC5EaXNwbGF5Iiwib3BlbmlkIiwiamF2YS1oZWxsby13b3JsZC5EZWxldGUiLCJqYXZhLWhlbGxvLXdvcmxkLkNyZWF0ZSJdLCJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6IjEwMDIxOTEiLCJ1c2VyX25hbWUiOiJXT0xGR0FORyIsImVtYWlsIjoiV09MRkdBTkdAdW5rbm93biIsImlhdCI6MTQ0MjkxMjI0NCwiZXhwIjoxNDQyOTU1MzIyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIiwiaGRiLm5hbWVkdXNlci5zYW1sIjoiPD94bWwgdmVyc2lvbj1cIjEuMFwiIGVuY29kaW5nPVwiVVRGLThcIj8-PHNhbWwyOkFzc2VydGlvbiB4bWxuczpzYW1sMj1cInVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb25cIiBJRD1cIl9mOTQ2YmM3Yi05MzM5LTQyMDAtYjdlYi0xYmJhNjU4MDEwNmJcIiBJc3N1ZUluc3RhbnQ9XCIyMDE1LTA5LTIyVDA4OjU1OjIyLjc1NVpcIiBWZXJzaW9uPVwiMi4wXCI-PHNhbWwyOklzc3Vlcj54czItbG9naW4tdGVzdC5jZi5zYXAtY2YuY29tLXNhbWwtbG9naW48L3NhbWwyOklzc3Vlcj48ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPVwiaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI1wiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMVwiLz48ZHM6UmVmZXJlbmNlIFVSST1cIiNfZjk0NmJjN2ItOTMzOS00MjAwLWI3ZWItMWJiYTY1ODAxMDZiXCI-PGRzOlRyYW5zZm9ybXM-PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZVwiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT1cImh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuI1wiLz48L2RzOlRyYW5zZm9ybXM-PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09XCJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMVwiLz48ZHM6RGlnZXN0VmFsdWU-bkNCRk5hUjR5ZlhIYUlDdHZaaDZUVnBYbUl3PTwvZHM6RGlnZXN0VmFsdWU-PC9kczpSZWZlcmVuY2U-PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5DVkp2SVBnZk9ZNnZtRjg5VGZtcHhZbUlWQWFRM2JtSlRhamVOTnVNRUMxWE50SndPenoyT2VGNnhWVjFvWkk1MzZLQURNeVBmTlZwRG5tdVBaQUVzNnFJRzdLVkZlWCtQMTNHUVgyMzdobURxeDFVZHRuTTk0Q2E4eVd5YnRhSXhjWStPMjBQYkFFa1RaQVpyUCtyczBtTjc4QUlGTk1KYStzSVpOYm5MdVU9PC9kczpTaWduYXR1cmVWYWx1ZT48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdD48c2FtbDI6TmFtZUlEIEZvcm1hdD1cInVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkXCI-V09MRkdBTkc8L3NhbWwyOk5hbWVJRD48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9XCJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyXCI-PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMTI6NTU6MjIuNzU1WlwiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24-PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT1cIjIwMTUtMDktMjJUMDg6NTU6MjIuNzU1WlwiIE5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMTI6NTU6MjIuNzU1WlwiLz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PVwiMjAxNS0wOS0yMlQwODo1NToyMi43NTdaXCIgU2Vzc2lvbk5vdE9uT3JBZnRlcj1cIjIwMTUtMDktMjJUMDk6MDA6MjIuNzU3WlwiPjxzYW1sMjpBdXRobkNvbnRleHQ-PHNhbWwyOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWwyOkF1dGhuQ29udGV4dD48L3NhbWwyOkF1dGhuU3RhdGVtZW50Pjwvc2FtbDI6QXNzZXJ0aW9uPiIsImF1ZCI6WyJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiamF2YS1oZWxsby13b3JsZCIsIm9wZW5pZCJdfQ.MOpwrUUoUYYYzvpZ72LmL8yakMcErTr1yI5bd_qbask
\ No newline at end of file