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

Issue:
App is not starting on cloud foundry without dummy values for services.identity
Hello colleagues,

I have added all the dependencies mentioned here: https://github.com/SAP/cloud-security-xsuaa-integration/blob/main/spring-security/README.md

But did not get the application running in cloud foundry, I also tried different combinations  of the properties `sap.spring.security.hybrid.auto` and `sap.spring.security.xsuaa.flows.auto`. Because our application is only bound against csuaa I set the parameters like:

`sap.spring.security.hybrid.auto: true`
`sap.spring.security.xsuaa.flows.auto: false`

But with or without the parameters I always got an error message during startup. Without the parameters set I got a error that client ID is missing, with the parameters set, that some configuration class could not be found.

Only if I set the following properties in my application.yaml it works:

```
sap.security.services.identity:
  clientid:  irrelevant
  domains:
    - doesntmatter
```

The missing client Id did not come from the xsuaa instance, there a client Id was found, but from the IAS service configuration which was not found.
It took me several hours to find this solution and the questions are now:

1. Is there a better way without the properteies for `sap.security.services.identity` to get the application running only with binding to xsuaa?
2. If not, could you include a better way for the future?

Regards
André

**Question Summary**
If applicable, please mention:
- client library: java-security:2.11.11
- Environment: Cloud Foundry



Pull request description:
[spring-security] HybridJwtDecoder should support xsuaa only
[x] revert: 1f0cdc9
[x] add test for HybridJwtDecoder

fixes #790 

Patch:
diff --git a/java-api/src/main/java/com/sap/cloud/security/x509/Certificate.java b/java-api/src/main/java/com/sap/cloud/security/x509/Certificate.java
index 6ec360b4..609baca1 100644
--- a/java-api/src/main/java/com/sap/cloud/security/x509/Certificate.java
+++ b/java-api/src/main/java/com/sap/cloud/security/x509/Certificate.java
@@ -5,7 +5,6 @@
  */
 package com.sap.cloud.security.x509;
 
-
 import java.util.Map;
 
 /**
@@ -22,8 +21,8 @@ public interface Certificate {
 	 *
 	 * @return the thumbprint
 	 * @throws InvalidCertificateException
-	 *             if error occurs while encoding X509 certificate
-	 *             or when a particular cryptographic algorithm is not supported
+	 *             if error occurs while encoding X509 certificate or when a
+	 *             particular cryptographic algorithm is not supported
 	 */
 	String getThumbprint() throws InvalidCertificateException;
 
@@ -37,8 +36,9 @@ public interface Certificate {
 	/**
 	 * Gets certificate subject DN map.
 	 *
-	 * @return the subject DN map with entries such as ["CN"]["common name"], or ["OU"]["1234-5678"]
+	 * @return the subject DN map with entries such as ["CN"]["common name"], or
+	 *         ["OU"]["1234-5678"]
 	 */
-	Map<String,String> getSubjectDNMap();
+	Map<String, String> getSubjectDNMap();
 
 }
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java
index 321feb70..b77ff7ac 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java
@@ -63,7 +63,8 @@ class JwtSignatureValidator implements Validator<Token> {
 		String jwksUri;
 		String keyId;
 
-		if (Service.IAS == configuration.getService() && !token.getIssuer().equals("" + configuration.getUrl()) && token.getZoneId() == null) { // lgtm[java/dereferenced-value-may-be-null]
+		if (Service.IAS == configuration.getService() && !token.getIssuer().equals("" + configuration.getUrl())
+				&& token.getZoneId() == null) { // lgtm[java/dereferenced-value-may-be-null]
 			return createInvalid("Error occurred during signature validation: OIDC token must provide zone_uuid.");
 		}
 		try {
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidator.java
index fe19d0f0..2bb39ea5 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidator.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidator.java
@@ -9,6 +9,7 @@ import com.sap.cloud.security.token.validation.ValidationResult;
 import com.sap.cloud.security.token.validation.ValidationResults;
 import com.sap.cloud.security.token.validation.Validator;
 import com.sap.cloud.security.x509.Certificate;
+import com.sap.cloud.security.xsuaa.Assertions;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -29,11 +30,10 @@ import javax.annotation.Nullable;
 public class JwtX5tValidator implements Validator<Token> {
 
 	private static final Logger LOGGER = LoggerFactory.getLogger(JwtX5tValidator.class);
-	private final OAuth2ServiceConfiguration config;
-
+	static final String VALIDATION_FAILED = "Certificate thumbprint validation failed";
 
 	public JwtX5tValidator(OAuth2ServiceConfiguration config) {
-		this.config = config;
+		Assertions.assertNotNull(config, "Service configuration must not be null");
 	}
 
 	/**
@@ -51,26 +51,23 @@ public class JwtX5tValidator implements Validator<Token> {
 	 */
 	@Override
 	public ValidationResult validate(Token token) {
+		if (token == null) {
+			return ValidationResults.createInvalid("No token passed to validate certificate thumbprint");
+		}
 		String tokenX5t = extractCnfThumbprintFromToken(token);
 		LOGGER.debug("Token 'cnf' thumbprint: {}", tokenX5t);
-		if (tokenX5t != null) {
-			Certificate clientCertificate = SecurityContext.getClientCertificate();
-			if (clientCertificate == null) {
-				LOGGER.error("Client certificate missing from SecurityContext");
-				return ValidationResults.createInvalid("Certificate validation failed");
-			}
-			String clientCertificateX5t = clientCertificate.getThumbprint();
-			if (clientCertificateX5t.equals(tokenX5t)) {
-				if (token.getAudiences().size() == 1 && token.getAudiences().contains(config.getClientId())) {
-					return ValidationResults.createValid();
-				}
-				LOGGER.error("Audience validation failed -> \"aud\": {} != \"clientid\": \"{}\"", token.getAudiences(), config.getClientId());
-			} else {
-				LOGGER.error("Thumbprint validation failed -> x5t from token: \"{}\" != thumbprint from client certificate: \"{}\"", tokenX5t,
-						clientCertificateX5t);
-			}
+		if (tokenX5t == null) {
+			return ValidationResults.createInvalid("Token doesn't contain certificate thumbprint confirmation method");
+		}
+		Certificate clientCertificate = SecurityContext.getClientCertificate();
+		if (clientCertificate == null) {
+			return ValidationResults.createInvalid("Client certificate missing from SecurityContext");
+		}
+		String clientCertificateX5t = clientCertificate.getThumbprint();
+		if (clientCertificateX5t.equals(tokenX5t)) {
+			return ValidationResults.createValid();
 		}
-		return ValidationResults.createInvalid("Certificate validation failed");
+		return ValidationResults.createInvalid(VALIDATION_FAILED);
 	}
 
 	/**
diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java b/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java
index 8f7318e9..aac753fd 100644
--- a/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java
+++ b/spring-security/src/main/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfiguration.java
@@ -59,7 +59,7 @@ public class XsuaaTokenFlowAutoConfiguration {
 	}
 
 	@Bean
-	@Conditional({ PropertyConditions.class })
+	@Conditional(PropertyConditions.class)
 	public XsuaaTokenFlows xsuaaTokenFlows(RestOperations xsuaaRestOperations) {
 		logger.debug("auto-configures XsuaaTokenFlows using {} based restOperations",
 				xsuaaConfig.getClientIdentity().isCertificateBased() ? "certificate" : "client secret");
@@ -76,7 +76,7 @@ public class XsuaaTokenFlowAutoConfiguration {
 	 * @return the {@link RestOperations} instance.
 	 */
 	@Bean
-	@Conditional({ OnNotX509CredentialTypeCondition.class })
+	@Conditional({ OnSecretCredentialTypeCondition.class, NoClientCertificateCondition.class })
 	@ConditionalOnMissingBean
 	public RestOperations restOperations() {
 		logger.warn("In productive environment provide a well configured client secret based RestOperations bean");
@@ -90,7 +90,6 @@ public class XsuaaTokenFlowAutoConfiguration {
 	 * @return the {@link RestOperations} instance.
 	 */
 	@Bean
-	@ConditionalOnProperty(prefix = "sap.security.services.xsuaa", name = "credential-type", havingValue = "x509")
 	@ConditionalOnClass(name = "org.apache.http.impl.client.CloseableHttpClient")
 	@ConditionalOnMissingBean
 	public RestOperations mtlsRestOperations(XsuaaServiceConfiguration xsuaaConfig) {
@@ -98,7 +97,7 @@ public class XsuaaTokenFlowAutoConfiguration {
 		return SpringHttpClient.getInstance().create(xsuaaConfig.getClientIdentity());
 	}
 
-	private static class OnNotX509CredentialTypeCondition implements Condition {
+	private static class OnSecretCredentialTypeCondition implements Condition {
 		@Override
 		public boolean matches(ConditionContext context, @Nonnull AnnotatedTypeMetadata metadata) {
 			CredentialType credentialType = CredentialType
@@ -108,17 +107,25 @@ public class XsuaaTokenFlowAutoConfiguration {
 		}
 	}
 
+	private static class NoClientCertificateCondition implements Condition {
+		@Override
+		public boolean matches(ConditionContext context, @Nonnull AnnotatedTypeMetadata metadata) {
+			return context.getEnvironment().getProperty("sap.security.services.xsuaa.certificate") == null &&
+					context.getEnvironment().getProperty("sap.security.services.xsuaa[0].certificate") == null;
+		}
+	}
+
 	private static class PropertyConditions extends AnyNestedCondition {
 
 		public PropertyConditions() {
 			super(ConfigurationPhase.REGISTER_BEAN);
 		}
 
-		@ConditionalOnProperty(prefix = "sap.security.services", name = "xsuaa[0].clientsecret")
+		@ConditionalOnProperty(prefix = "sap.security.services", name = "xsuaa[0].clientid")
 		static class MultipleBindingsCondition {
 		}
 
-		@ConditionalOnProperty(prefix = "sap.security.services", name = "xsuaa.clientsecret")
+		@ConditionalOnProperty(prefix = "sap.security.services", name = "xsuaa.clientid")
 		static class SingleBindingCondition {
 		}
 	}
diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoder.java b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoder.java
index 3cbcb53a..630b0689 100644
--- a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoder.java
+++ b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoder.java
@@ -15,6 +15,8 @@ import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.util.Assert;
 
+import javax.annotation.Nullable;
+
 /**
  * Internal class that decodes and validates the provided encoded token using
  * {@code java-security} client library.<br>
@@ -39,7 +41,8 @@ public class HybridJwtDecoder implements JwtDecoder {
 	 *            set of validators that should be used to validate an ias oidc
 	 *            token.
 	 */
-	public HybridJwtDecoder(CombiningValidator<Token> xsuaaValidator, CombiningValidator<Token> iasValidator) {
+	public HybridJwtDecoder(CombiningValidator<Token> xsuaaValidator,
+			@Nullable CombiningValidator<Token> iasValidator) {
 		xsuaaTokenValidators = xsuaaValidator;
 		iasTokenValidators = iasValidator;
 	}
@@ -58,18 +61,21 @@ public class HybridJwtDecoder implements JwtDecoder {
 		ValidationResult validationResult;
 		switch (token.getService()) {
 		case IAS:
+			if (iasTokenValidators == null) {
+				throw new BadJwtException("Tokens issued by IAS service aren't accepted");
+			}
 			validationResult = iasTokenValidators.validate(token);
 			break;
 		case XSUAA:
 			validationResult = xsuaaTokenValidators.validate(token);
 			break;
 		default:
-			throw new BadJwtException("The token of service " + token.getService() + " is not supported.");
+			throw new BadJwtException("Tokens issued by " + token.getService() + " service aren't supported.");
 		}
 		if (validationResult.isErroneous()) {
 			throw new BadJwtException("The token is invalid: " + validationResult.getErrorDescription());
 		}
-		logger.debug("The token of service {} was successfully validated.", token.getService());
+		logger.debug("Token issued by {} service was successfully validated.", token.getService());
 		return jwt;
 	}
 
diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilder.java b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilder.java
index 16cd386d..d3ff61a4 100644
--- a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilder.java
+++ b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilder.java
@@ -117,28 +117,35 @@ public class JwtDecoderBuilder {
 	 * @return JwtDecoder
 	 */
 	public JwtDecoder build() {
-		JwtValidatorBuilder iasValidatorBuilder = JwtValidatorBuilder.getInstance(iasConfiguration)
-				.withCacheConfiguration(tokenKeyCacheConfiguration)
-				.withHttpClient(httpClient);
-		for (ValidationListener listener : validationListeners) {
-			iasValidatorBuilder.withValidatorListener(listener);
+		JwtValidatorBuilder iasValidatorBuilder = null;
+		if (iasConfiguration != null && !iasConfiguration.getProperties().isEmpty()) {
+			iasValidatorBuilder = initializeBuilder(iasConfiguration);
 		}
 		if (xsuaaConfigurations != null && !xsuaaConfigurations.isEmpty()) {
 			int index = 0;
-			JwtValidatorBuilder xsuaaValidatorBuilder = JwtValidatorBuilder.getInstance(xsuaaConfigurations.get(index))
-					.withCacheConfiguration(tokenKeyCacheConfiguration)
-					.withHttpClient(httpClient);
+			JwtValidatorBuilder xsuaaValidatorBuilder = initializeBuilder(xsuaaConfigurations.get(index));
 			for (OAuth2ServiceConfiguration xsuaaConfig : xsuaaConfigurations) {
 				if (index++ != 0) {
 					xsuaaValidatorBuilder.configureAnotherServiceInstance(xsuaaConfig);
 				}
 			}
-			for (ValidationListener listener : validationListeners) {
-				xsuaaValidatorBuilder.withValidatorListener(listener);
-			}
 			return new HybridJwtDecoder(xsuaaValidatorBuilder.build(),
-					iasValidatorBuilder.build());
+					iasValidatorBuilder != null ? iasValidatorBuilder.build() : null);
+		}
+		if (iasValidatorBuilder == null) {
+			throw new IllegalStateException("There is no xsuaa and no identity service config.");
+		}
+		return new IasJwtDecoder(iasValidatorBuilder.build()); // lgtm [java/dereferenced-value-may-be-null] - line 136
+	}
+
+
+	private JwtValidatorBuilder initializeBuilder(OAuth2ServiceConfiguration config) {
+		JwtValidatorBuilder builder = JwtValidatorBuilder.getInstance(config)
+				.withCacheConfiguration(tokenKeyCacheConfiguration)
+				.withHttpClient(httpClient);
+		for (ValidationListener listener : validationListeners) {
+			builder.withValidatorListener(listener);
 		}
-		return new IasJwtDecoder(iasValidatorBuilder.build());
+		return builder;
 	}
 }
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefault.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefault.java
index 0354b44e..69df32b0 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefault.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefault.java
@@ -5,18 +5,18 @@
  */
 package com.sap.cloud.security.xsuaa;
 
-import com.sap.cloud.security.config.ClientCertificate;
-import com.sap.cloud.security.config.ClientCredentials;
-import com.sap.cloud.security.config.ClientIdentity;
-import com.sap.cloud.security.config.CredentialType;
+import com.sap.cloud.security.config.*;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Configuration;
 
 import java.net.URI;
+import java.util.Objects;
 
 @Configuration
 public class XsuaaServiceConfigurationDefault implements XsuaaServiceConfiguration {
 
+	static final String VCAP_SERVICES_CREDENTIALS = "xsuaa credentials from VCAP_SERVICES/secret must not be null";
+
 	@Value("${xsuaa.clientid:}")
 	private String clientId;
 
@@ -41,7 +41,7 @@ public class XsuaaServiceConfigurationDefault implements XsuaaServiceConfigurati
 	@Value("${xsuaa.verificationkey:}")
 	private String verificationKey;
 
-	@Value("${xsuaa.credentialtype:#{null}}")
+	@Value("${xsuaa.credential-type:#{null}}")
 	private String credentialType;
 
 	@Value("${xsuaa.certurl:#{null}}")
@@ -101,4 +101,31 @@ public class XsuaaServiceConfigurationDefault implements XsuaaServiceConfigurati
 		return identity;
 	}
 
+	/**
+	 * This only supports read from VCAP_SERVICES in cf environment or read from
+	 * secrets in kubernetes environment.
+	 * 
+	 * @param name
+	 *            of the credential property
+	 * @return the property value or null if not found
+	 */
+	@Override
+	public String getProperty(String name) {
+		return Objects.requireNonNull(Environments.getCurrent().getXsuaaConfiguration(), VCAP_SERVICES_CREDENTIALS)
+				.getProperty(name);
+	}
+
+	/**
+	 * This only supports VCAP_SERVICES in cf environment or read from secrets in
+	 * kubernetes environment.
+	 * 
+	 * @param name
+	 *            of the credential property
+	 * @return false if property doesn't exist
+	 */
+	@Override
+	public boolean hasProperty(String name) {
+		return Objects.requireNonNull(Environments.getCurrent().getXsuaaConfiguration(), VCAP_SERVICES_CREDENTIALS)
+				.hasProperty(name);
+	}
 }
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfiguration.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfiguration.java
index 0eced4a5..b4fd7e80 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfiguration.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfiguration.java
@@ -81,7 +81,7 @@ public class XsuaaAutoConfiguration {
 	 * @return the {@link RestOperations} instance.
 	 */
 	@Bean
-	@Conditional({ OnNotX509CredentialTypeCondition.class })
+	@Conditional({ OnSecretCredentialTypeCondition.class, NoClientCertificateCondition.class })
 	@ConditionalOnMissingBean
 	@ConditionalOnBean(XsuaaServiceConfiguration.class)
 	public RestOperations xsuaaRestOperations() {
@@ -96,7 +96,6 @@ public class XsuaaAutoConfiguration {
 	 * @return the {@link RestOperations} instance.
 	 */
 	@Bean
-	@ConditionalOnProperty(prefix = "xsuaa", name = "credential-type", havingValue = "x509")
 	@ConditionalOnMissingBean
 	@ConditionalOnClass(name = "org.apache.http.impl.client.CloseableHttpClient")
 	@ConditionalOnBean(XsuaaServiceConfiguration.class)
@@ -105,7 +104,7 @@ public class XsuaaAutoConfiguration {
 		return SpringHttpClient.getInstance().create(xsuaaServiceConfiguration.getClientIdentity());
 	}
 
-	private static class OnNotX509CredentialTypeCondition implements Condition {
+	private static class OnSecretCredentialTypeCondition implements Condition {
 		@Override
 		public boolean matches(ConditionContext context, @Nonnull AnnotatedTypeMetadata metadata) {
 			CredentialType credentialType = CredentialType
@@ -115,4 +114,12 @@ public class XsuaaAutoConfiguration {
 		}
 	}
 
+	private static class NoClientCertificateCondition implements Condition {
+		@Override
+		public boolean matches(ConditionContext context, @Nonnull AnnotatedTypeMetadata metadata) {
+			return context.getEnvironment().getProperty("xsuaa.certificate") == null &&
+					context.getEnvironment().getProperty("xsuaa.key") == null;
+		}
+	}
+
 }
\ No newline at end of file
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/Assertions.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/Assertions.java
index 6dd27222..77ffa279 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/Assertions.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/Assertions.java
@@ -24,7 +24,7 @@ public class Assertions {
 		}
 	}
 
-	public static void assertNotEmpty(List<String> list, String message) {
+	public static void assertNotEmpty(List<?> list, String message) {
 		if (list == null || list.isEmpty()) {
 			throw new IllegalArgumentException(message);
 		}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java
index c6a1ee18..13bd738b 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java
@@ -10,7 +10,8 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 /**
- * Service that targets Identity service (xsuaa and identity) to request Json Web Keys.
+ * Service that targets Identity service (xsuaa and identity) to request Json
+ * Web Keys.
  */
 public interface OAuth2TokenKeyService {
 
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpoints.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpoints.java
index f01fed85..7a478b65 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpoints.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpoints.java
@@ -104,6 +104,7 @@ public class XsuaaDefaultEndpoints implements OAuth2ServiceEndpointsProvider {
 
 	@Override
 	public URI getJwksUri() {
+		assertNotNull(baseUri, "XsuaaDefaultEndpoints.getJwksUri() requires baseUri not to be null.");
 		return expandPath(baseUri, KEYSET_ENDPOINT);
 	}
 
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/UriUtil.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/UriUtil.java
index f751872e..85943a2b 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/UriUtil.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/util/UriUtil.java
@@ -69,7 +69,8 @@ public class UriUtil {
 	 */
 	// TODO rename to getUriWithPathAppended
 	@Nonnull
-	public static URI expandPath(URI baseUri, String pathToAppend) {
+	public static URI expandPath(@Nonnull URI baseUri, String pathToAppend) {
+		Assertions.assertNotNull(baseUri, "the baseUri parameter must not be null");
 		try {
 			String newPath = baseUri.getPath() + pathToAppend;
 			return new URI(baseUri.getScheme(), baseUri.getUserInfo(), baseUri.getHost(), baseUri.getPort(),


Test patch:
diff --git a/env/src/test/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilderTest.java b/env/src/test/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilderTest.java
index ec5a4d50..881adb07 100644
--- a/env/src/test/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilderTest.java
+++ b/env/src/test/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilderTest.java
@@ -5,6 +5,7 @@
  */
 package com.sap.cloud.security.config;
 
+import com.sap.cloud.security.xsuaa.client.XsuaaDefaultEndpoints;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -197,7 +198,31 @@ public class OAuth2ServiceConfigurationBuilderTest {
 		assertThat(configuration.getClientId()).isEqualTo("base-client-id");
 		assertThat(configuration.getClientSecret()).isEqualTo("base-client-secret");
 		assertThat(configuration.getUrl()).isEqualTo(URI.create("http://url.base"));
+		assertThat(configuration.getCertUrl()).isNull();
 		assertThat(configuration.getProperty("testing-key")).isEqualTo("base-value");
 		assertThat(configuration.getDomains()).isEqualTo(Collections.EMPTY_LIST);
 	}
+
+	@Test
+	public void x509IntegrationTest() {
+		String url = "http://the.cert.Url.org";
+
+		OAuth2ServiceConfiguration configuration = OAuth2ServiceConfigurationBuilder.forService(Service.XSUAA)
+				.withClientId("client-id")
+				.withCertUrl(url)
+				.withCertificate("cert")
+				.withPrivateKey("key")
+				.withCredentialType(CredentialType.X509)
+				.build();
+
+		assertThat(configuration.getClientId()).isEqualTo("client-id");
+		assertThat(configuration.getClientIdentity().getCertificate()).isEqualTo("cert");
+		assertThat(configuration.getClientIdentity().getKey()).isEqualTo("key");
+		assertThat(configuration.getUrl()).isNull();
+		assertThat(configuration.getCertUrl()).isEqualTo(URI.create(url));
+		assertThat(configuration.getCredentialType()).isEqualTo(CredentialType.X509);
+
+		XsuaaDefaultEndpoints endpoints = new XsuaaDefaultEndpoints(configuration);
+		assertThat(endpoints.getAuthorizeEndpoint().toString()).isEqualTo("http://the.cert.Url.org/oauth/authorize");
+	}
 }
\ No newline at end of file
diff --git a/java-security/src/test/java/com/sap/cloud/security/servlet/IasTokenAuthenticatorX509Test.java b/java-security/src/test/java/com/sap/cloud/security/servlet/IasTokenAuthenticatorX509Test.java
index fac26fed..46ba1d7f 100644
--- a/java-security/src/test/java/com/sap/cloud/security/servlet/IasTokenAuthenticatorX509Test.java
+++ b/java-security/src/test/java/com/sap/cloud/security/servlet/IasTokenAuthenticatorX509Test.java
@@ -80,14 +80,14 @@ class IasTokenAuthenticatorX509Test {
 		TokenAuthenticationResult response = cut.validateRequest(httpRequest, HTTP_RESPONSE);
 
 		assertThat(response.getUnauthenticatedReason())
-				.contains("Error during token validation: Certificate validation failed");
+				.contains("Error during token validation: Client certificate missing");
 		assertThat(response.isAuthenticated()).isFalse();
 
 		when(httpRequest.getHeader(FWD_CLIENT_CERT_HEADER)).thenReturn("");
 		TokenAuthenticationResult response2 = cut.validateRequest(httpRequest, HTTP_RESPONSE);
 
 		assertThat(response2.getUnauthenticatedReason())
-				.contains("Error during token validation: Certificate validation failed");
+				.contains("Error during token validation: Client certificate missing");
 		assertThat(response2.isAuthenticated()).isFalse();
 
 	}
@@ -100,7 +100,7 @@ class IasTokenAuthenticatorX509Test {
 		TokenAuthenticationResult response = cut.validateRequest(httpRequest, HTTP_RESPONSE);
 
 		assertThat(response.getUnauthenticatedReason())
-				.contains("Error during token validation: Certificate validation failed");
+				.contains("Error during token validation: Client certificate missing");
 		assertThat(response.isAuthenticated()).isFalse();
 	}
 
@@ -112,7 +112,7 @@ class IasTokenAuthenticatorX509Test {
 		TokenAuthenticationResult response = cut.validateRequest(httpRequest, HTTP_RESPONSE);
 
 		assertThat(response.getUnauthenticatedReason())
-				.contains("Error during token validation: Certificate validation failed");
+				.contains("Error during token validation: Certificate thumbprint validation failed");
 		assertThat(response.isAuthenticated()).isFalse();
 	}
 
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidatorTest.java
index f0f92e09..99590205 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidatorTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtX5tValidatorTest.java
@@ -23,10 +23,9 @@ import org.mockito.internal.util.collections.Sets;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
-import java.util.Collections;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static com.sap.cloud.security.token.validation.validators.JwtX5tValidator.VALIDATION_FAILED;
+import static org.junit.jupiter.api.Assertions.*;
 
 class JwtX5tValidatorTest {
 
@@ -36,7 +35,6 @@ class JwtX5tValidatorTest {
 	private static X509Certificate x509;
 	private static final Token TOKEN = Mockito.mock(Token.class);
 	private static final JsonObject JSON_MOCK = Mockito.mock(JsonObject.class);
-	private static final String INVALID_MESSAGE = "Certificate validation failed";
 	private static final String X5T_VALIDATOR_DISABLED = "X5tValidator is not enabled";
 
 	@BeforeAll
@@ -50,94 +48,72 @@ class JwtX5tValidatorTest {
 				.withClientId("myClientId")
 				.build();
 		CUT = new JwtX5tValidator(configuration);
-		Mockito.when(JSON_MOCK.getAsString(TokenClaims.CNF_X5T)).thenReturn("fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM");
+		Mockito.when(JSON_MOCK.getAsString(TokenClaims.CNF_X5T))
+				.thenReturn("fU-XoQlhMTpQsz9ArXl6zHIpMGuRO4ExLKdLRTc5VjM");
 	}
 
 	@Test
-	void validateToken_ValidToken_validX509() {
-		Token token = Token.create(tokenWithX5t);
-		SecurityContext.setClientCertificate(x509);
-		ValidationResult result = CUT.validate(token);
-		assertTrue(result.isValid());
+	void given_NoConfig() {
+		assertThrows(
+				IllegalArgumentException.class,
+				() -> new JwtX5tValidator(null),
+				"Service configuration must not be null");
 	}
 
 	@Test
-	void validateToken_ValidToken_invalidX509() {
-		Token token = Token.create(tokenWithX5t);
-		SecurityContext.setClientCertificate(null);
-		ValidationResult result = CUT.validate(token);
+	void given_NoToken() {
+		ValidationResult result = CUT.validate(null);
 		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
+		assertEquals("No token passed to validate certificate thumbprint", result.getErrorDescription());
 	}
 
 	@Test
-	void validateToken_validCnf_validX509_MultipleAud() {
+	void given_ValidToken_validX509() {
+		Token token = Token.create(tokenWithX5t);
 		SecurityContext.setClientCertificate(x509);
-		Mockito.when(TOKEN.getAudiences()).thenReturn(Sets.newSet("aud1", "aud2"));
-		Mockito.when(TOKEN.getClaimAsJsonObject(TokenClaims.CNF)).thenReturn(JSON_MOCK);
-		ValidationResult result = CUT.validate(TOKEN);
-		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
+		ValidationResult result = CUT.validate(token);
+		assertTrue(result.isValid());
 	}
 
 	@Test
-	void validateToken_validCnf_validX509_EmptyAud() {
-		SecurityContext.setClientCertificate(x509);
-		Mockito.when(TOKEN.getAudiences()).thenReturn(Collections.emptySet());
-		Mockito.when(TOKEN.getClaimAsJsonObject(TokenClaims.CNF)).thenReturn(JSON_MOCK);
-		ValidationResult result = CUT.validate(TOKEN);
+	void given_ValidToken_invalidX509() {
+		Token token = Token.create(tokenWithX5t);
+		SecurityContext.setClientCertificate(null);
+		ValidationResult result = CUT.validate(token);
 		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
+		assertEquals("Client certificate missing from SecurityContext", result.getErrorDescription());
 	}
 
 	@Test
-	void validateToken_InvalidCnf_validX509() {
+	void given_InvalidCnf_validX509() {
 		Token token = Token.create(tokenWithInvalidX5t);
 		SecurityContext.setClientCertificate(x509);
 		ValidationResult result = CUT.validate(token);
 		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
+		assertEquals(VALIDATION_FAILED, result.getErrorDescription());
 	}
 
 	@Test
-	void validateToken_InvalidCnf_invalidX509() {
+	void given_InvalidCnf_invalidX509() {
 		Token token = Token.create(tokenWithInvalidX5t);
 		SecurityContext.setClientCertificate(null);
 		ValidationResult result = CUT.validate(token);
 		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
+		assertEquals("Client certificate missing from SecurityContext", result.getErrorDescription());
 	}
 
 	@Test
-	void validateToken_NoCnf_validAud() {
+	void given_NoCnf_validAud() {
 		Mockito.when(JSON_MOCK.getAsString(TokenClaims.CNF_X5T)).thenReturn(null);
 		Mockito.when(TOKEN.getAudiences()).thenReturn(Sets.newSet("myClientId"));
 		ValidationResult result = CUT.validate(TOKEN);
 		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
-	}
-
-	@Test
-	void validateToken_NoCnf_MultipleAud() {
-		Mockito.when(JSON_MOCK.getAsString(TokenClaims.CNF_X5T)).thenReturn(null);
-		Mockito.when(TOKEN.getAudiences()).thenReturn(Sets.newSet("aud1", "aud2"));
-		ValidationResult result = CUT.validate(TOKEN);
-		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
-	}
-
-	@Test
-	void validateToken_NoCnf_EmptyAud() {
-		Mockito.when(JSON_MOCK.getAsString(TokenClaims.CNF_X5T)).thenReturn(null);
-		Mockito.when(TOKEN.getAudiences()).thenReturn(Collections.emptySet());
-		ValidationResult result = CUT.validate(TOKEN);
-		assertTrue(result.isErroneous());
-		assertEquals(INVALID_MESSAGE, result.getErrorDescription());
+		assertEquals("Token doesn't contain certificate thumbprint confirmation method", result.getErrorDescription());
 	}
 
-	@Disabled("until proofOfPossesion validator subchain is implemented")
+	@Disabled("until proofOfPossession validator subchain is implemented")
 	@Test
-	void validateToken_validatorDisabled() {
+	void given_validatorDisabled() {
 		Token token = Token.create(tokenWithX5t);
 		ValidationResult result = CUT.validate(token);
 		assertTrue(result.isErroneous());
diff --git a/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java b/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java
index a3638381..965035de 100644
--- a/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/x509/X509CertificateTest.java
@@ -41,13 +41,16 @@ class X509CertificateTest {
 
 	@Test
 	void getSubjectDN() {
-		assertThat(cut.getSubjectDN()).isEqualTo("CN=bdcd300c-b202-4a7a-bb95-2a7e6d15fe47/2b585405-d391-4986-b76d-b4f24685f3c8, L=aoxk2addh.accounts400.ondemand.com, OU=8e1affb2-62a1-43cc-a687-2ba75e4b3d84, OU=Canary, OU=SAP Cloud Platform Clients, O=SAP SE, C=DE");
+		assertThat(cut.getSubjectDN()).isEqualTo(
+				"CN=bdcd300c-b202-4a7a-bb95-2a7e6d15fe47/2b585405-d391-4986-b76d-b4f24685f3c8, L=aoxk2addh.accounts400.ondemand.com, OU=8e1affb2-62a1-43cc-a687-2ba75e4b3d84, OU=Canary, OU=SAP Cloud Platform Clients, O=SAP SE, C=DE");
 	}
 
 	@Test
 	void getSubjectDNMap() {
 		assertThat(cut.getSubjectDNMap().size()).isEqualTo(5);
-		assertThat(cut.getSubjectDNMap().get("CN")).isEqualTo("bdcd300c-b202-4a7a-bb95-2a7e6d15fe47/2b585405-d391-4986-b76d-b4f24685f3c8");
-		assertThat(cut.getSubjectDNMap().get("OU")).isEqualTo("8e1affb2-62a1-43cc-a687-2ba75e4b3d84,Canary,SAP Cloud Platform Clients");
+		assertThat(cut.getSubjectDNMap().get("CN"))
+				.isEqualTo("bdcd300c-b202-4a7a-bb95-2a7e6d15fe47/2b585405-d391-4986-b76d-b4f24685f3c8");
+		assertThat(cut.getSubjectDNMap().get("OU"))
+				.isEqualTo("8e1affb2-62a1-43cc-a687-2ba75e4b3d84,Canary,SAP Cloud Platform Clients");
 	}
 }
\ No newline at end of file
diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfigurationTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfigurationTest.java
index c60cbe8b..82577abb 100644
--- a/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfigurationTest.java
+++ b/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/HybridIdentityServicesAutoConfigurationTest.java
@@ -57,10 +57,10 @@ class HybridIdentityServicesAutoConfigurationTest {
 	}
 
 	@Test
-	void autoConfigurationDisabledForMultipleXsuaaServices() {
-		List<String> mt_properties = new ArrayList<>();
+	void autoConfigurationForIdentityAndSingleXsuaaService() {
 		WebApplicationContextRunner mt_runner;
 
+		List<String> mt_properties = new ArrayList<>();
 		mt_properties.add("sap.security.services.xsuaa[0].url:http://localhost");
 		mt_properties.add("sap.security.services.xsuaa[0].uaadomain:localhost");
 		mt_properties.add("sap.security.services.xsuaa[0].xsappname:theAppName");
@@ -74,7 +74,7 @@ class HybridIdentityServicesAutoConfigurationTest {
 
 		mt_runner.run(context -> {
 			assertFalse(context.containsBean("hybridJwtDecoder"));
-			assertNotNull(context.getBean("hybridJwtDecoderMultiXsuaaServices", HybridJwtDecoder.class));
+			assertTrue(context.containsBean("hybridJwtDecoderMultiXsuaaServices"));
 		});
 	}
 
@@ -104,6 +104,51 @@ class HybridIdentityServicesAutoConfigurationTest {
 		});
 	}
 
+	@Test
+	void autoConfigurationUsesDecoderForSingleXsuaaService() {
+		WebApplicationContextRunner mt_runner;
+
+		List<String> mt_properties = new ArrayList<>();
+		mt_properties.add("sap.security.services.xsuaa.url:http://localhost");
+		mt_properties.add("sap.security.services.xsuaa.uaadomain:localhost");
+		mt_properties.add("sap.security.services.xsuaa.xsappname:theAppName");
+		mt_properties.add("sap.security.services.xsuaa.clientid:xsuaacid");
+
+		mt_runner = new WebApplicationContextRunner()
+				.withPropertyValues(mt_properties.toArray(new String[0]))
+				.withConfiguration(AutoConfigurations.of(HybridIdentityServicesAutoConfiguration.class));
+
+		mt_runner.run(context -> {
+			assertTrue(context.containsBean("hybridJwtDecoder"));
+			assertFalse(context.containsBean("hybridJwtDecoderMultiXsuaaServices"));
+			assertEquals(context.getBean(HybridJwtDecoder.class),
+					context.getBean("hybridJwtDecoder"));
+		});
+	}
+
+	@Test
+	void autoConfigurationUsesDecoderForMultipleXsuaaServices() {
+		WebApplicationContextRunner mt_runner;
+
+		List<String> mt_properties = new ArrayList<>();
+		mt_properties.add("sap.security.services.xsuaa[0].url:http://localhost");
+		mt_properties.add("sap.security.services.xsuaa[0].uaadomain:localhost");
+		mt_properties.add("sap.security.services.xsuaa[0].xsappname:theAppName");
+		mt_properties.add("sap.security.services.xsuaa[0].clientid:xsuaacid");
+		mt_properties.add("sap.security.services.xsuaa[1].clientid:cid2");
+
+		mt_runner = new WebApplicationContextRunner()
+				.withPropertyValues(mt_properties.toArray(new String[0]))
+				.withConfiguration(AutoConfigurations.of(HybridIdentityServicesAutoConfiguration.class));
+
+		mt_runner.run(context -> {
+			assertFalse(context.containsBean("hybridJwtDecoder"));
+			assertTrue(context.containsBean("hybridJwtDecoderMultiXsuaaServices"));
+			assertEquals(context.getBean(HybridJwtDecoder.class),
+					context.getBean("hybridJwtDecoderMultiXsuaaServices"));
+		});
+	}
+
 	@Test
 	void userConfigurationCanOverrideDefaultBeans() {
 		runner.withUserConfiguration(UserConfiguration.class)
diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfigurationTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfigurationTest.java
index f9eed0dd..785c3c80 100644
--- a/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfigurationTest.java
+++ b/spring-security/src/test/java/com/sap/cloud/security/spring/autoconfig/XsuaaTokenFlowAutoConfigurationTest.java
@@ -72,9 +72,11 @@ class XsuaaTokenFlowAutoConfigurationTest {
 
 	@Test
 	void configures_xsuaaMtlsRestTemplate() {
-		runner
-				.withPropertyValues("sap.spring.security.xsuaa.flows.auto:true")
-				.withPropertyValues("sap.security.services.xsuaa.credential-type:x509")
+		runner = new ApplicationContextRunner()
+				.withConfiguration(AutoConfigurations.of(HybridIdentityServicesAutoConfiguration.class,
+						XsuaaTokenFlowAutoConfiguration.class))
+				.withPropertyValues("sap.security.services.xsuaa.certurl:https://domain.cert.auth.com")
+				.withPropertyValues("sap.security.services.xsuaa.clientid:cid")
 				.withPropertyValues("sap.security.services.xsuaa.certificate:" + cert)
 				.withPropertyValues("sap.security.services.xsuaa.key:" + key)
 				.run((context) -> {
@@ -90,7 +92,7 @@ class XsuaaTokenFlowAutoConfigurationTest {
 	}
 
 	@Test
-	void autoConfigurationDisabledWhenNoClientSecretIsGiven() {
+	void autoConfigurationDisabledWhenNoClientIdIsGiven() {
 		WebApplicationContextRunner mt_runner;
 
 		mt_runner = new WebApplicationContextRunner()
@@ -105,7 +107,7 @@ class XsuaaTokenFlowAutoConfigurationTest {
 		WebApplicationContextRunner mt_runner;
 
 		List<String> mt_properties = new ArrayList<>(properties);
-		mt_properties.add("sap.security.services.xsuaa[0].url:http://localhost");
+		mt_properties.add("sap.security.services.xsuaa[0].certurl:https://domain.cert.auth.com");
 		mt_properties.add("sap.security.services.xsuaa[0].clientid:cid");
 		mt_properties.add("sap.security.services.xsuaa[0].clientsecret:pwd");
 		mt_properties.add("sap.security.services.xsuaa[1].clientid:cid");
@@ -118,6 +120,26 @@ class XsuaaTokenFlowAutoConfigurationTest {
 		mt_runner.run(context -> assertTrue(context.containsBean("xsuaaTokenFlows")));
 	}
 
+	@Test
+	void configures_xsuaaMtlsRestTemplateForMultipleXsuaaServicesAsPrimary() {
+		WebApplicationContextRunner mt_runner;
+
+		List<String> mt_properties = new ArrayList<>(properties);
+		mt_properties.add("sap.security.services.xsuaa[0].url:http://localhost");
+		mt_properties.add("sap.security.services.xsuaa[0].clientid:cid");
+		mt_properties.add("sap.security.services.xsuaa[0].certificate:cert");
+		mt_properties.add("sap.security.services.xsuaa[0].key:key");
+		mt_properties.add("sap.security.services.xsuaa[1].clientid:cid");
+
+		mt_runner = new WebApplicationContextRunner()
+				.withPropertyValues(mt_properties.toArray(new String[0]))
+				.withConfiguration(AutoConfigurations.of(HybridIdentityServicesAutoConfiguration.class,
+						XsuaaTokenFlowAutoConfiguration.class));
+
+		mt_runner.run(context -> assertTrue(context.containsBean("xsuaaTokenFlows")));
+		mt_runner.run(context -> assertTrue(context.containsBean("mtlsRestOperations")));
+	}
+
 	@Test
 	void userConfigurationCanOverrideDefaultBeans() {
 		runner.withUserConfiguration(UserConfiguration.class)
diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoderTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoderTest.java
index 526ef650..9f8beacf 100644
--- a/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoderTest.java
+++ b/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/HybridJwtDecoderTest.java
@@ -5,7 +5,6 @@
  */
 package com.sap.cloud.security.spring.token.authentication;
 
-import com.sap.cloud.security.json.JsonParsingException;
 import com.sap.cloud.security.test.JwtGenerator;
 import com.sap.cloud.security.token.Token;
 import com.sap.cloud.security.token.TokenClaims;
@@ -27,11 +26,12 @@ import static org.mockito.Mockito.when;
 
 class HybridJwtDecoderTest {
 	JwtGenerator jwtGenerator = JwtGenerator.getInstance(IAS, "theClientId");
+	CombiningValidator<Token> combiningValidator;
 	HybridJwtDecoder cut;
 
 	@BeforeEach
 	void setup() {
-		CombiningValidator<Token> combiningValidator = Mockito.mock(CombiningValidator.class);
+		combiningValidator = Mockito.mock(CombiningValidator.class);
 		when(combiningValidator.validate(any())).thenReturn(ValidationResults.createValid());
 
 		cut = new HybridJwtDecoder(combiningValidator, combiningValidator);
@@ -61,7 +61,6 @@ class HybridJwtDecoderTest {
 
 	@Test
 	void decodeInvalidToken_throwsAccessDeniedException() {
-		CombiningValidator<Token> combiningValidator = Mockito.mock(CombiningValidator.class);
 		when(combiningValidator.validate(any())).thenReturn(ValidationResults.createInvalid("error"));
 		cut = new HybridJwtDecoder(combiningValidator, combiningValidator);
 		String encodedToken = jwtGenerator.createToken().getTokenValue();
@@ -80,13 +79,23 @@ class HybridJwtDecoderTest {
 
 	@Test
 	void decodeWithCorruptToken_throwsBadJwtException() {
-		String encodedToken = jwtGenerator
-				.withClaimValue(TokenClaims.EXPIRATION, "")
-				.createToken().getTokenValue();
-
 		assertThrows(BadJwtException.class, () -> cut.decode("Bearer e30="));
 		assertThrows(BadJwtException.class, () -> cut.decode("Bearer"));
 		assertThrows(BadJwtException.class, () -> cut.decode(null));
 		assertThrows(BadJwtException.class, () -> cut.decode("Bearerabc"));
 	}
+
+	@Test
+	void instantiateForXsuaaOnly() {
+		cut = new HybridJwtDecoder(combiningValidator, null);
+
+		// IAS token can't be validated
+		String encodedIasToken = jwtGenerator.createToken().getTokenValue();
+		assertThrows(BadJwtException.class, () -> cut.decode(encodedIasToken));
+
+		// XSUAA token can be validated
+		String encodedXsuaaToken = JwtGenerator.getInstance(XSUAA, "theClientId").createToken().getTokenValue();
+		assertEquals("theClientId", cut.decode(encodedXsuaaToken).getClaim(TokenClaims.AUTHORIZATION_PARTY));
+
+	}
 }
\ No newline at end of file
diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilderTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilderTest.java
index a8800c93..9d33f1f2 100644
--- a/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilderTest.java
+++ b/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/JwtDecoderBuilderTest.java
@@ -38,21 +38,20 @@ class JwtDecoderBuilderTest {
 	}
 
 	@Test
-	void buildHybridWithoutConfiguration_IllegalArgumentException() {
-		assertThrows(IllegalArgumentException.class, () -> cut.build());
+	void buildWithoutConfiguration_IllegalStateException() {
+		assertThrows(IllegalStateException.class, () -> cut.build());
 	}
 
 	@Test
 	void buildHybridWithoutConfiguration() {
-		OAuth2ServiceConfiguration iasConfiguration = OAuth2ServiceConfigurationBuilder
+		OAuth2ServiceConfiguration configuration = OAuth2ServiceConfigurationBuilder
 				.forService(Service.IAS)
 				.withClientId("clientId")
 				.withProperty(CFConstants.URL, "https://domain.myauth.com")
-				// .withProperty(CFConstants.IAS.DOMAIN, "myauth.com")
 				.build();
 
-		cut.withIasServiceConfiguration(iasConfiguration);
-		cut.withXsuaaServiceConfiguration(iasConfiguration);
+		cut.withIasServiceConfiguration(configuration);
+		cut.withXsuaaServiceConfiguration(configuration);
 		cut.withValidationListener(Mockito.mock(ValidationListener.class));
 		JwtDecoder decoder = cut.build();
 		assertTrue(decoder instanceof HybridJwtDecoder);
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefaultTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefaultTest.java
index 21d194c0..8342a996 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefaultTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationDefaultTest.java
@@ -10,12 +10,13 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import java.net.URI;
 
+import static com.sap.cloud.security.xsuaa.XsuaaServiceConfigurationDefault.VCAP_SERVICES_CREDENTIALS;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 @RunWith(SpringJUnit4ClassRunner.class)
 @TestPropertySource(properties = { "xsuaa.clientid=client", "xsuaa.certificate=cert", "xsuaa.key=key",
-		"xsuaa.certurl=https://my.cert.authentication.sap.com", "xsuaa.credentialtype=x509" })
+		"xsuaa.certurl=https://my.cert.authentication.sap.com", "xsuaa.credential-type=x509" })
 @ContextConfiguration(classes = { XsuaaServiceConfigurationDefault.class })
 public class XsuaaServiceConfigurationDefaultTest {
 
@@ -52,8 +53,20 @@ public class XsuaaServiceConfigurationDefaultTest {
 
 	@Test
 	public void unsupportedMethods() {
-		assertThatThrownBy(() -> cut.hasProperty("")).isInstanceOf(UnsupportedOperationException.class);
-		assertThatThrownBy(() -> cut.getProperty("")).isInstanceOf(UnsupportedOperationException.class);
 		assertThatThrownBy(() -> cut.getProperties()).isInstanceOf(UnsupportedOperationException.class);
 	}
+
+	@Test
+	public void hasPropertyRequiresEnvironmentVar() {
+		assertThatThrownBy(() -> cut.hasProperty("clientid"))
+				.isInstanceOf(NullPointerException.class)
+				.hasMessageContaining(VCAP_SERVICES_CREDENTIALS);
+	}
+
+	@Test
+	public void getPropertyRequiresEnvironmentVar() {
+		assertThatThrownBy(() -> cut.getProperty("clientid"))
+				.isInstanceOf(NullPointerException.class)
+				.hasMessageContaining(VCAP_SERVICES_CREDENTIALS);
+	}
 }
\ No newline at end of file
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationFromEnvTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationFromEnvTest.java
new file mode 100644
index 00000000..091ff536
--- /dev/null
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfigurationFromEnvTest.java
@@ -0,0 +1,41 @@
+package com.sap.cloud.security.xsuaa;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.contrib.java.lang.system.EnvironmentVariables;
+
+import static com.sap.cloud.security.config.cf.CFConstants.CLIENT_ID;
+import static com.sap.cloud.security.config.cf.CFConstants.VCAP_SERVICES;
+import static com.sap.cloud.security.config.cf.CFConstants.XSUAA.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class XsuaaServiceConfigurationFromEnvTest {
+	XsuaaServiceConfigurationDefault cut;
+
+	@Rule
+	public final EnvironmentVariables environmentVariables = new EnvironmentVariables();
+
+	@Before
+	public void setup() {
+		environmentVariables.set(VCAP_SERVICES,
+				"{\"xsuaa\":[{\"credentials\":{\"apiurl\":\"https://api.mydomain.com\",\"tenantid\":\"tenant-id\",\"subaccountid\":\"subaccount-id\",\"clientid\":\"client-id\"},\"tags\":[\"xsuaa\"]}]}");
+		cut = new XsuaaServiceConfigurationDefault();
+	}
+
+	@Test
+	public void getProperty() {
+		assertThat(cut.getProperty(API_URL)).isEqualTo("https://api.mydomain.com");
+		assertThat(cut.getProperty(SUBACCOUNT_ID)).isEqualTo("subaccount-id");
+		assertThat(cut.getProperty(TENANT_ID)).isEqualTo("tenant-id");
+		assertThat(cut.getProperty(CLIENT_ID)).isEqualTo("client-id");
+		assertThat(cut.getProperty("unknownProp")).isNull();
+	}
+
+	@Test
+	public void hasProperty() {
+		assertThat(cut.hasProperty(API_URL)).isTrue();
+		assertThat(cut.hasProperty(SUBACCOUNT_ID)).isTrue();
+		assertThat(cut.hasProperty("unknownProp")).isFalse();
+	}
+}
\ No newline at end of file
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServicePropertySourceFactoryTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServicePropertySourceFactoryTest.java
index 9221c39a..69ff7aca 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServicePropertySourceFactoryTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/XsuaaServicePropertySourceFactoryTest.java
@@ -82,7 +82,7 @@ class TestConfiguration {
 	@Value("${xsuaa.key:}")
 	public String key;
 
-	@Value("${xsuaa.credentialtype:}")
+	@Value("${xsuaa.credential-type:}")
 	public String credentialType;
 
 	@Value("${xsuaa.certurl:}")
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfigurationTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfigurationTest.java
index b128af29..5a36537e 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfigurationTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfigurationTest.java
@@ -70,7 +70,7 @@ public class XsuaaAutoConfigurationTest {
 	}
 
 	@Test
-	public void configures_xsuaaMtlsRestTemplate() {
+	public void configures_xsuaaMtlsRestTemplateWithCredType() {
 		contextRunner
 				.withPropertyValues("spring.xsuaa.flows.auto:true")
 				.withPropertyValues("xsuaa.credential-type:x509")
@@ -84,6 +84,19 @@ public class XsuaaAutoConfigurationTest {
 				});
 	}
 
+	@Test
+	public void configures_xsuaaMtlsRestTemplate() {
+		contextRunner
+				.withPropertyValues("xsuaa.clientid:client")
+				.withPropertyValues("xsuaa.certificate:" + cert)
+				.withPropertyValues("xsuaa.key:" + key)
+				.withPropertyValues("xsuaa.certurl:https://domain.cert.authentication.sap.com")
+				.run((context) -> {
+					assertThat(context).hasSingleBean(RestOperations.class);
+					assertThat(context).hasBean("xsuaaMtlsRestOperations");
+				});
+	}
+
 	@Test
 	public void configures_xsuaaServiceConfiguration_withProperties() {
 		contextRunner.withClassLoader(new FilteredClassLoader(CloseableHttpClient.class))
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpointsTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpointsTest.java
index e14572f7..7eab99d3 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpointsTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpointsTest.java
@@ -65,6 +65,17 @@ public class XsuaaDefaultEndpointsTest {
 		new XsuaaDefaultEndpoints(null, CERT_URL);
 	}
 
+	@Test(expected = IllegalArgumentException.class)
+	public void getJwksUri_throwsException_whenBaseUriIsNull() {
+		Mockito.when(oAuth2ServiceConfiguration.getCertUrl()).thenReturn(URI.create(CERT_URL));
+		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(X509);
+
+		cut = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
+
+		assertThat(cut.getTokenEndpoint().toString(), is(CERT_URL + "/oauth/token")); // ok
+		cut.getJwksUri(); // raise exception
+	}
+
 	@Test
 	@Deprecated
 	public void getEndpoint_forBaseUrl() {