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

Issue:
XsuaaTokenFlows: Deprecated constructors for client credential based authentication
Hello Colleagues,
could you please provide an example for the proper usage of Example Usage of XsuaaServiceConfiguration?

The background of this request is the following:
We have developed a service which in turn is calling an instance of the Market Rates Management (MRM) service. For the MRM call we first get the access token from the uaa instance bound to the MRM service. Currently we use the following coding for this purpose:

```java
XsuaaTokenFlows tokenFlows =
        new XsuaaTokenFlows(
            new DefaultOAuth2TokenService(),
            new XsuaaDefaultEndpoints(uaaUrl),
            new ClientCredentials(clientId, clientSecret));

return tokenFlows.clientCredentialsTokenFlow().execute().getAccessToken();
```

In this solution `uaaUrl`, `clientId` and `clientSecret` are coming from `uaa` environment variables (VCAP_SERVICES):
```java
@Value("${vcap.services.bpi-market-rates.credentials.uaa.url}")
String uaaUrl;
```

The solution was/is working as expected but unfortunately the used constructor version of XsuaaTokenFlows is now deprecated. For the alternative constructor we need an instance of OAuth2ServiceConfigurationProperties. I have seen that XsuaaServiceConfiguration is a specialization of OAuth2ServiceConfigurationProperties. My question now is, how do we get a proper instance of XsuaaServiceConfiguration?

Seeing that the SpringBoot annotation @ConfigurationProperties("sap.security.services.xsuaa") is used I’m wondering from where the properties are really taken from and how can I get an instance which then contains the uaa-credentials of the MRM service and not the ones of my own service?

And maybe one additional remark: at the end my own service should be called from the BTP Job Scheduler - I'm not sure if this adds additional complexity for getting the MRM credentials.

Thanks in advance for your help!

Best regards,
Sven.

P.S.: If have seen that there was already [a similar question](https://github.com/SAP/cloud-security-xsuaa-integration/issues/701) raised but this doesn't answer my question above.


Pull request description:
XsuaaDefaultEndpoints: provide another constructor to compensate the deprecated one
fixes #707

Hi @liga-oz 
Alternatively we could provide a new constructor
````
XsuaaDefaultEndpoints(String baseUri, @Nullable String certUri)
````

Furthermore, we could set the default certUri by adding the  that `https://subdomain.authentication.cert.domain`

Patch:
diff --git a/java-api/pom.xml b/java-api/pom.xml
index 3a7ef4f6..7998e7d6 100644
--- a/java-api/pom.xml
+++ b/java-api/pom.xml
@@ -69,11 +69,6 @@
 			<artifactId>commons-io</artifactId>
 			<scope>test</scope>
 		</dependency>
-		<dependency> <!-- check if it's still needed when slf4j-api 1.8 is available -->
-			<groupId>org.slf4j</groupId>
-			<artifactId>slf4j-simple</artifactId>
-			<scope>test</scope>
-		</dependency>
 	</dependencies>
 
 	<build>
diff --git a/java-api/src/main/java/com/sap/cloud/security/token/Token.java b/java-api/src/main/java/com/sap/cloud/security/token/Token.java
index 4afd8a5f..db062d54 100644
--- a/java-api/src/main/java/com/sap/cloud/security/token/Token.java
+++ b/java-api/src/main/java/com/sap/cloud/security/token/Token.java
@@ -14,12 +14,11 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.io.Serializable;
 import java.security.Principal;
+import java.security.ProviderException;
 import java.time.Instant;
 import java.util.*;
 
-import static com.sap.cloud.security.token.TokenClaims.AUDIENCE;
-import static com.sap.cloud.security.token.TokenClaims.AUTHORIZATION_PARTY;
-import static com.sap.cloud.security.token.TokenClaims.ISSUER;
+import static com.sap.cloud.security.token.TokenClaims.*;
 
 /**
  * Represents a JSON Web Token (JWT).
@@ -34,6 +33,8 @@ public interface Token extends Serializable {
 		}
 	};
 
+	String DEFAULT_TOKEN_FACTORY = "com.sap.cloud.security.servlet.HybridTokenFactory";
+
 	/**
 	 * Creates a token instance based on TokenFactory implementation.
 	 * 
@@ -45,6 +46,15 @@ public interface Token extends Serializable {
 		if (services.isEmpty()) {
 			throw new ProviderNotFoundException("No TokenFactory implementation found in the classpath");
 		}
+		if (services.size() > 2) {
+			throw new ProviderException("More than 1 Custom TokenFactory service provider found. There should be only one");
+		}
+		if (services.size() == 2) {
+			return services.stream()
+					.filter(tokenFactory -> !tokenFactory.getClass().getName()
+							.equals(DEFAULT_TOKEN_FACTORY))
+					.findFirst().get().create(jwt);
+		}
 		return services.get(0).create(jwt);
 	}
 
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 c7d8592e..19a4aa91 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
@@ -451,7 +451,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	 */
 	XsuaaTokenFlows getXsuaaTokenFlows(String baseUaaUrl, ClientIdentity clientIdentity) {
 		return new XsuaaTokenFlows(getOrCreateOAuth2TokenService(),
-				new XsuaaDefaultEndpoints(baseUaaUrl), clientIdentity);
+				new XsuaaDefaultEndpoints(baseUaaUrl, null), clientIdentity);
 	}
 
 	private String performTokenFlow(String baseUaaUrl, int tokenRequestType, String clientId, String clientSecret,
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java
index 4169534d..8fb41216 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtIssuerValidator.java
@@ -85,7 +85,8 @@ class JwtIssuerValidator implements Validator<Token> {
 			}
 			if (!issuer.startsWith("http")) {
 				return createInvalid(
-						"Issuer is not trusted because issuer '{}' does not provide a valid URI (missing http scheme). Please contact your Identity Provider Administrator.", issuer);
+						"Issuer is not trusted because issuer '{}' does not provide a valid URI (missing http scheme). Please contact your Identity Provider Administrator.",
+						issuer);
 			}
 			issuerUri = new URI(issuer);
 			if (issuerUri.getQuery() == null && issuerUri.getFragment() == null && issuerUri.getHost() != null) {
diff --git a/pom.xml b/pom.xml
index 7c59f5d6..a5bec274 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,7 +63,7 @@
 		<spring.security.version>5.5.3</spring.security.version>
 		<spring.security.oauth2.version>2.5.1.RELEASE</spring.security.oauth2.version>
 		<spring.security.jwt.version>1.1.1.RELEASE</spring.security.jwt.version>
-		<reactor.version>3.4.11</reactor.version>
+		<reactor.version>3.4.12</reactor.version>
 		<log4j.version>2.14.1</log4j.version>
 		<slf4j.api.version>1.7.32</slf4j.api.version> <!--see also here http://www.slf4j.org/faq.html#changesInVersion18 -->
 		<org.json.version>20210307</org.json.version>
@@ -79,7 +79,7 @@
 		<hamcrest.version>1.3</hamcrest.version>
 		<mockito.version>3.12.4</mockito.version>
 		<assertj.version>3.21.0</assertj.version>
-		<reactor.test.version>3.4.11</reactor.test.version>
+		<reactor.test.version>3.4.12</reactor.test.version>
 		<wiremock.version>2.31.0</wiremock.version>
 		<skipTests>false</skipTests>
 		<jacoco.skip>${skipTests}</jacoco.skip>
@@ -391,7 +391,7 @@
 						<plugin>
 							<groupId>org.owasp</groupId> <!--scans for vulnerabilities-->
 							<artifactId>dependency-check-maven</artifactId>
-							<version>6.4.1</version>
+							<version>6.5.0</version>
 							<executions>
 								<execution>
 									<goals>
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfiguration.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfiguration.java
index 9e31bf6d..5ed98540 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfiguration.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfiguration.java
@@ -5,6 +5,7 @@
  */
 package com.sap.cloud.security.xsuaa;
 
+import com.sap.cloud.security.config.ClientIdentity;
 import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
 import com.sap.cloud.security.config.Service;
 
@@ -65,6 +66,14 @@ public interface XsuaaServiceConfiguration extends OAuth2ServiceConfiguration {
 		throw new UnsupportedOperationException("hasProperty method is not supported");
 	}
 
+	@Override
+	default ClientIdentity getClientIdentity() {
+		throw new UnsupportedOperationException(
+				"This default method needs to be overridden to be used! Default method from " +
+						"com.sap.cloud.security.config.OAuth2ServiceConfiguration#getClientIdentity() " +
+						"is not compatible with XsuaaServiceConfiguration interface");
+	}
+
 	@Override
 	default Service getService() {
 		throw new UnsupportedOperationException("getService method is not supported");
diff --git a/token-client/pom.xml b/token-client/pom.xml
index 0a9b8b28..211b4f43 100644
--- a/token-client/pom.xml
+++ b/token-client/pom.xml
@@ -97,11 +97,6 @@
 			<artifactId>spring-security-oauth2-jose</artifactId>
 			<scope>test</scope>
 		</dependency>
-		<dependency> <!-- check if it's still needed when slf4j-api 1.8 is available -->
-			<groupId>org.slf4j</groupId>
-			<artifactId>slf4j-simple</artifactId>
-			<scope>test</scope>
-		</dependency>
 	</dependencies>
 
 	<build>
diff --git a/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java b/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java
index 17b55f27..79bb944e 100644
--- a/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java
+++ b/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java
@@ -25,6 +25,7 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
 
 	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultHttpClientFactory.class);
 
+	@Override
 	public CloseableHttpClient createClient(ClientIdentity clientIdentity) throws HttpClientException {
 		LOGGER.warn("In productive environment provide well configured HttpClientFactory service");
 		if (clientIdentity != null && clientIdentity.isCertificateBased()) {
diff --git a/token-client/src/main/java/com/sap/cloud/security/client/HttpClientFactory.java b/token-client/src/main/java/com/sap/cloud/security/client/HttpClientFactory.java
index 57681c1e..f8d37a0b 100644
--- a/token-client/src/main/java/com/sap/cloud/security/client/HttpClientFactory.java
+++ b/token-client/src/main/java/com/sap/cloud/security/client/HttpClientFactory.java
@@ -11,6 +11,7 @@ import com.sap.cloud.security.token.ProviderNotFoundException;
 import org.apache.http.impl.client.CloseableHttpClient;
 import org.slf4j.LoggerFactory;
 
+import java.security.ProviderException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.ServiceLoader;
@@ -19,6 +20,7 @@ import java.util.ServiceLoader;
  * Represents a {@link CloseableHttpClient} creation interface.
  */
 public interface HttpClientFactory {
+
 	@SuppressWarnings("unchecked")
 	List<HttpClientFactory> services = new ArrayList() {
 		{
@@ -28,6 +30,9 @@ public interface HttpClientFactory {
 		}
 	};
 
+	String DEFAULT_HTTP_CLIENT_FACTORY = "com.sap.cloud.security.client.DefaultHttpClientFactory";
+
+
 	/**
 	 * Provides CloseableHttpClient based on ClientIdentity details. For
 	 * ClientIdentity that is certificate based it will resolve https client using
@@ -48,6 +53,16 @@ public interface HttpClientFactory {
 		if (services.isEmpty()) {
 			throw new ProviderNotFoundException("No HttpClientFactory service could be found in the classpath");
 		}
+		if (services.size() > 2) {
+			throw new ProviderException(
+					"More than 1 Custom HttpClientFactory service provider found. There should be only one");
+		}
+		if (services.size() == 2) {
+			return services.stream()
+					.filter(httpClientFactory -> !httpClientFactory.getClass().getName()
+							.equals(DEFAULT_HTTP_CLIENT_FACTORY))
+					.findFirst().get().createClient(clientIdentity);
+		}
 		return services.get(0).createClient(clientIdentity);
 	}
 
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 5db2bd78..f01fed85 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
@@ -11,6 +11,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import java.net.URI;
 
 import static com.sap.cloud.security.xsuaa.Assertions.assertNotNull;
@@ -32,16 +33,33 @@ public class XsuaaDefaultEndpoints implements OAuth2ServiceEndpointsProvider {
 	 *            - the base URI of XSUAA. Based on the base URI the tokenEndpoint,
 	 *            authorize and key set URI (JWKS) will be derived.
 	 * @deprecated gets removed with the major release 3.0.0 Use instead
-	 *             {@link #XsuaaDefaultEndpoints(OAuth2ServiceConfiguration)}
+	 *             {@link #XsuaaDefaultEndpoints(String, String)}
 	 */
 	@Deprecated
 	public XsuaaDefaultEndpoints(URI baseUri) {
 		assertNotNull(baseUri, "XSUAA base URI must not be null.");
-		LOGGER.debug("Xsuaa default service endpoint = {}", baseUri);
+		LOGGER.warn("Using deprecated constructor to initialize xsuaa default service endpoint = {}", baseUri);
 		this.baseUri = baseUri;
 		this.certUri = null;
 	}
 
+	/**
+	 * Creates a new XsuaaDefaultEndpoints.
+	 *
+	 * @param baseUri
+	 *            - the base URI of XSUAA. Based on the base URI the tokenEndpoint,
+	 *            authorize and key set URI (JWKS) will be derived.
+	 * @param certUri
+	 *            - the cert URI of XSUAA. It is required in case of X.509
+	 *            certificate based authentication.
+	 */
+	public XsuaaDefaultEndpoints(@Nonnull String baseUri, @Nullable String certUri) {
+		assertNotNull(baseUri, "XSUAA base URI must not be null.");
+		LOGGER.debug("Xsuaa default service endpoint: base url = {}, (cert url = {})", baseUri, certUri);
+		this.baseUri = URI.create(baseUri);
+		this.certUri = certUri != null ? URI.create(certUri) : null;
+	}
+
 	/**
 	 * Creates a new XsuaaDefaultEndpoints.
 	 *
@@ -67,7 +85,7 @@ public class XsuaaDefaultEndpoints implements OAuth2ServiceEndpointsProvider {
 	 *            - the base URI of XSUAA. Based on the base URI the tokenEndpoint,
 	 *            authorize and key set URI (JWKS) will be derived.
 	 * @deprecated gets removed with the major release 3.0.0 Use instead
-	 *             {@link #XsuaaDefaultEndpoints(OAuth2ServiceConfiguration)}
+	 *             {@link #XsuaaDefaultEndpoints(String, String)}
 	 */
 	@Deprecated
 	public XsuaaDefaultEndpoints(String baseUri) {


Test patch:
diff --git a/java-api/src/test/java/com/sap/cloud/security/servlet/HybridTokenFactory.java b/java-api/src/test/java/com/sap/cloud/security/servlet/HybridTokenFactory.java
new file mode 100644
index 00000000..b33357cf
--- /dev/null
+++ b/java-api/src/test/java/com/sap/cloud/security/servlet/HybridTokenFactory.java
@@ -0,0 +1,19 @@
+/**
+ * SPDX-FileCopyrightText: 2018-2021 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors
+ * <p>
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.sap.cloud.security.servlet;
+
+import com.sap.cloud.security.token.AccessToken;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.TokenFactory;
+import org.mockito.Mockito;
+
+public class HybridTokenFactory implements TokenFactory {
+
+	@Override
+	public Token create(String jwtToken) {
+		return Mockito.mock(AccessToken.class);
+	}
+}
diff --git a/java-api/src/test/java/com/sap/cloud/security/token/TokenTest.java b/java-api/src/test/java/com/sap/cloud/security/token/TokenTest.java
index 8aecb404..7265e60f 100644
--- a/java-api/src/test/java/com/sap/cloud/security/token/TokenTest.java
+++ b/java-api/src/test/java/com/sap/cloud/security/token/TokenTest.java
@@ -7,6 +7,7 @@ package com.sap.cloud.security.token;
 
 import org.junit.Test;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 
 public class TokenTest {
@@ -18,5 +19,10 @@ public class TokenTest {
 
 		cut = Token.create("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
 		assertNotNull(cut);
+
+		// Assert that custom Token factory has a priority over default
+		// com.sap.cloud.security.servlet.HybridTokenFactory
+		assertFalse(cut.getClass().getName().contains("AccessToken"));
 	}
+
 }
diff --git a/java-api/src/test/resources/META-INF/services/com.sap.cloud.security.token.TokenFactory b/java-api/src/test/resources/META-INF/services/com.sap.cloud.security.token.TokenFactory
index c2cb9383..70fc90c0 100644
--- a/java-api/src/test/resources/META-INF/services/com.sap.cloud.security.token.TokenFactory
+++ b/java-api/src/test/resources/META-INF/services/com.sap.cloud.security.token.TokenFactory
@@ -1,1 +1,2 @@
+com.sap.cloud.security.servlet.HybridTokenFactory
 com.sap.cloud.security.token.test.CustomTokenFactory
\ No newline at end of file
diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java
index d85895bc..092606e1 100644
--- a/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java
+++ b/java-security-test/src/main/java/com/sap/cloud/security/test/SecurityTest.java
@@ -270,7 +270,7 @@ public class SecurityTest
 		}
 		// TODO return JSON Media type
 		OAuth2ServiceEndpointsProvider endpointsProvider = new XsuaaDefaultEndpoints(
-				String.format(LOCALHOST_PATTERN, wireMockServer.port()));
+				String.format(LOCALHOST_PATTERN, wireMockServer.port()), null);
 		wireMockServer.stubFor(get(urlEqualTo(endpointsProvider.getJwksUri().getPath()))
 				.willReturn(aResponse().withBody(createDefaultTokenKeyResponse())
 						.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.value())));
diff --git a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
index cd11650f..e6b23288 100644
--- a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
+++ b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
@@ -135,7 +135,7 @@ public class SecurityTestRuleTest {
 		Token token = cut.getJwtGeneratorFromFile("/token.json").createToken();
 
 		String baseUrl = cut.base.wireMockServer.baseUrl();
-		URI jwksUrl = new XsuaaDefaultEndpoints(baseUrl).getJwksUri();
+		URI jwksUrl = new XsuaaDefaultEndpoints(baseUrl, null).getJwksUri();
 		assertThat(token.getHeaderParameterAsString(TokenHeader.JWKS_URL)).isEqualTo(jwksUrl.toString());
 		assertThat(token.getClaimAsString(TokenClaims.ISSUER)).isEqualTo(baseUrl);
 	}
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/DummyXsuaaServiceConfiguration.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/DummyXsuaaServiceConfiguration.java
index 6a33f4f6..8c9f8d3a 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/DummyXsuaaServiceConfiguration.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/DummyXsuaaServiceConfiguration.java
@@ -5,6 +5,8 @@
  */
 package com.sap.cloud.security.xsuaa;
 
+import com.sap.cloud.security.config.ClientCredentials;
+import com.sap.cloud.security.config.ClientIdentity;
 import com.sap.cloud.security.config.CredentialType;
 
 public class DummyXsuaaServiceConfiguration implements XsuaaServiceConfiguration {
@@ -56,9 +58,9 @@ public class DummyXsuaaServiceConfiguration implements XsuaaServiceConfiguration
 		return null;
 	}
 
-	// TODO remove once credential-type is available in IAS configuration
 	@Override
-	public String getProperty(String name) {
-		return null;
+	public ClientIdentity getClientIdentity() {
+		return new ClientCredentials(getClientId(), getClientSecret());
 	}
+
 }
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 88bee6f0..21d194c0 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
@@ -14,7 +14,8 @@ 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"})
+@TestPropertySource(properties = { "xsuaa.clientid=client", "xsuaa.certificate=cert", "xsuaa.key=key",
+		"xsuaa.certurl=https://my.cert.authentication.sap.com", "xsuaa.credentialtype=x509" })
 @ContextConfiguration(classes = { XsuaaServiceConfigurationDefault.class })
 public class XsuaaServiceConfigurationDefaultTest {
 
diff --git a/token-client/src/test/java/com/sap/cloud/security/client/HttpClientFactoryTest.java b/token-client/src/test/java/com/sap/cloud/security/client/HttpClientFactoryTest.java
new file mode 100644
index 00000000..9313a1b6
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/client/HttpClientFactoryTest.java
@@ -0,0 +1,27 @@
+/**
+ * SPDX-FileCopyrightText: 2018-2021 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors
+ * <p>
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.sap.cloud.security.client;
+
+import com.sap.cloud.security.config.ClientCredentials;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class HttpClientFactoryTest {
+
+	@Test
+	public void create() {
+		CloseableHttpClient cut = HttpClientFactory.create(new ClientCredentials("clientId", "secret"));
+		assertNotNull(cut);
+
+		// Assert that custom HttpClientFactory factory has a priority over default
+		// com.sap.cloud.security.client.DefaultHttpClientFactory
+		assertFalse(cut.getClass().getName().contains("InternalHttpClient"));
+		assertTrue(cut.getClass().getName().contains("CloseableHttpClient$MockitoMock"));
+	}
+
+}
\ No newline at end of file
diff --git a/token-client/src/test/java/com/sap/cloud/security/client/TestHttpClientFactory.java b/token-client/src/test/java/com/sap/cloud/security/client/TestHttpClientFactory.java
new file mode 100644
index 00000000..ee2d7f66
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/client/TestHttpClientFactory.java
@@ -0,0 +1,18 @@
+/**
+ * SPDX-FileCopyrightText: 2018-2021 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors
+ * <p>
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package com.sap.cloud.security.client;
+
+import com.sap.cloud.security.config.ClientIdentity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.mockito.Mockito;
+
+public class TestHttpClientFactory implements HttpClientFactory {
+
+	@Override
+	public CloseableHttpClient createClient(ClientIdentity clientIdentity) throws HttpClientException {
+		return Mockito.mock(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 17db8259..e14572f7 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
@@ -5,7 +5,6 @@
  */
 package com.sap.cloud.security.xsuaa.client;
 
-import com.sap.cloud.security.config.CredentialType;
 import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
 import org.junit.Before;
 import org.junit.Test;
@@ -13,6 +12,7 @@ import org.mockito.Mockito;
 
 import java.net.URI;
 
+import static com.sap.cloud.security.config.CredentialType.*;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 
@@ -21,73 +21,61 @@ public class XsuaaDefaultEndpointsTest {
 	private static OAuth2ServiceConfiguration oAuth2ServiceConfiguration;
 	private static final String URL = "https://subdomain.myauth.com";
 	private static final String CERT_URL = "https://subdomain.cert.myauth.com";
+	private OAuth2ServiceEndpointsProvider cut;
 
 	@Before
 	public void setUp() {
 		oAuth2ServiceConfiguration = Mockito.mock(OAuth2ServiceConfiguration.class);
-		Mockito.when(oAuth2ServiceConfiguration.getUrl()).thenReturn(URI.create(URL));
 	}
 
 	@Test
-	public void getTokenEndpoint() {
-		OAuth2ServiceEndpointsProvider cut = createXsuaaDefaultEndpointProvider(URL);
-
-		assertThat(cut.getTokenEndpoint().toString(), is(URL + "/oauth/token"));
+	public void getEndpoints() {
+		Mockito.when(oAuth2ServiceConfiguration.getUrl()).thenReturn(URI.create(URL));
+		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(INSTANCE_SECRET);
 
-		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(CredentialType.INSTANCE_SECRET);
-		OAuth2ServiceEndpointsProvider cut2 = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
+		cut = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
 
-		assertThat(cut2.getTokenEndpoint().toString(), is(URL + "/oauth/token"));
+		assertThat(cut.getTokenEndpoint().toString(), is(URL + "/oauth/token"));
+		assertThat(cut.getAuthorizeEndpoint().toString(), is(URL + "/oauth/authorize"));
+		assertThat(cut.getJwksUri().toString(), is(URL + "/token_keys"));
 	}
 
 	@Test
-	public void getTokenEndpointX509() {
+	public void getEndpoints_forX509OAuth2ServiceConfiguration() {
+		Mockito.when(oAuth2ServiceConfiguration.getUrl()).thenReturn(URI.create(URL));
 		Mockito.when(oAuth2ServiceConfiguration.getCertUrl()).thenReturn(URI.create(CERT_URL));
-		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(CredentialType.X509);
-		OAuth2ServiceEndpointsProvider cut = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
-		assertThat(cut.getTokenEndpoint().toString(), is(CERT_URL + "/oauth/token"));
-	}
+		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(X509);
 
-	@Test
-	public void getAuthorizeEndpoint() {
-		OAuth2ServiceEndpointsProvider cut = createXsuaaDefaultEndpointProvider(URL);
+		cut = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
 
-		assertThat(cut.getAuthorizeEndpoint().toString(), is(URL + "/oauth/authorize"));
-
-		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(CredentialType.INSTANCE_SECRET);
-		OAuth2ServiceEndpointsProvider cut2 = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
-
-		assertThat(cut2.getAuthorizeEndpoint().toString(), is(URL + "/oauth/authorize"));
+		assertThat(cut.getTokenEndpoint().toString(), is(CERT_URL + "/oauth/token"));
+		assertThat(cut.getAuthorizeEndpoint().toString(), is(CERT_URL + "/oauth/authorize"));
+		assertThat(cut.getJwksUri().toString(), is(URL + "/token_keys"));
 	}
 
 	@Test
-	public void getAuthorizeEndpointX509() {
-		Mockito.when(oAuth2ServiceConfiguration.getCertUrl()).thenReturn(URI.create(CERT_URL));
-		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(CredentialType.X509);
-		OAuth2ServiceEndpointsProvider cut2 = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
+	public void getEndpoint_forCertUrl() {
+		cut = new XsuaaDefaultEndpoints(URL, CERT_URL);
 
-		assertThat(cut2.getAuthorizeEndpoint().toString(), is(CERT_URL + "/oauth/authorize"));
+		assertThat(cut.getTokenEndpoint().toString(), is(CERT_URL + "/oauth/token"));
 	}
 
-	@Test
-	public void getJwksUri() {
-		OAuth2ServiceEndpointsProvider cut = createXsuaaDefaultEndpointProvider(URL);
-
-		assertThat(cut.getJwksUri().toString(), is(URL + "/token_keys"));
+	@Test(expected = IllegalArgumentException.class)
+	public void getEndpoint_throwsException_whenBaseUriIsNull() {
+		new XsuaaDefaultEndpoints(null, CERT_URL);
 	}
 
 	@Test
-	public void getJwksUriX509() {
-		Mockito.when(oAuth2ServiceConfiguration.getCertUrl()).thenReturn(URI.create(CERT_URL));
-		Mockito.when(oAuth2ServiceConfiguration.getCredentialType()).thenReturn(CredentialType.X509);
-		OAuth2ServiceEndpointsProvider cut2 = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
+	@Deprecated
+	public void getEndpoint_forBaseUrl() {
+		cut = new XsuaaDefaultEndpoints(URL);
 
-		assertThat(cut2.getJwksUri().toString(), is(URL + "/token_keys"));
+		assertThat(cut.getTokenEndpoint().toString(), is(URL + "/oauth/token"));
 	}
 
 	@Test
 	public void withEndingPathDelimiter() {
-		OAuth2ServiceEndpointsProvider cut = createXsuaaDefaultEndpointProvider("http://localhost:8080/uaa/");
+		cut = createXsuaaDefaultEndpointProvider("http://localhost:8080/uaa/");
 
 		assertThat(cut.getAuthorizeEndpoint().toString(), is("http://localhost:8080/uaa/oauth/authorize"));
 		assertThat(cut.getTokenEndpoint().toString(), is("http://localhost:8080/uaa/oauth/token"));
@@ -96,7 +84,7 @@ public class XsuaaDefaultEndpointsTest {
 
 	@Test
 	public void withQueryParameters() {
-		OAuth2ServiceEndpointsProvider cut = createXsuaaDefaultEndpointProvider("http://localhost:8080/uaa?abc=123");
+		cut = createXsuaaDefaultEndpointProvider("http://localhost:8080/uaa?abc=123");
 
 		assertThat(cut.getAuthorizeEndpoint().toString(), is("http://localhost:8080/uaa/oauth/authorize?abc=123"));
 		assertThat(cut.getTokenEndpoint().toString(), is("http://localhost:8080/uaa/oauth/token?abc=123"));
@@ -105,7 +93,7 @@ public class XsuaaDefaultEndpointsTest {
 
 	@Test
 	public void withQueryParametersAndEndingPathDelimiter() {
-		OAuth2ServiceEndpointsProvider cut = createXsuaaDefaultEndpointProvider(
+		cut = createXsuaaDefaultEndpointProvider(
 				"http://localhost:8080/uaa/?abc=123");
 
 		assertThat(cut.getAuthorizeEndpoint().toString(), is("http://localhost:8080/uaa/oauth/authorize?abc=123"));
@@ -114,6 +102,6 @@ public class XsuaaDefaultEndpointsTest {
 	}
 
 	private OAuth2ServiceEndpointsProvider createXsuaaDefaultEndpointProvider(String baseUri) {
-		return new XsuaaDefaultEndpoints(baseUri);
+		return new XsuaaDefaultEndpoints(baseUri, null);
 	}
 }
diff --git a/token-client/src/test/resources/META-INF/services/com.sap.cloud.security.client.HttpClientFactory b/token-client/src/test/resources/META-INF/services/com.sap.cloud.security.client.HttpClientFactory
new file mode 100644
index 00000000..6b945a92
--- /dev/null
+++ b/token-client/src/test/resources/META-INF/services/com.sap.cloud.security.client.HttpClientFactory
@@ -0,0 +1,2 @@
+com.sap.cloud.security.client.DefaultHttpClientFactory
+com.sap.cloud.security.client.TestHttpClientFactory
\ No newline at end of file