Repository: SAP/cloud-security-xsuaa-integration
Pull request #124 (2019-08-12 12:04:10)

Introduce new client lib "token-client" to server XSUAA token-endpoints
### Motivation
- have one compact [ interface / api](https://github.com/SAP/cloud-security-xsuaa-integration/blob/token-client/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java) to access token endpoints supported by XSUAA. It supports:
  - User token exchange
  - Refresh token
  - and client-credentials token

These CF UAA APIs are supported:
- version 4.30 (current) https://docs.cloudfoundry.org/api/uaa/version/4.30.0
- version 74.0 (latest) https://docs.cloudfoundry.org/api/uaa/version/74.0.0


### Consumers
- first "consumer": spring-xsuaa library
- planned "consumer": other CAP libraries, Java Security container lib, cloud-sdk?

### Dependencies
- https://mvnrepository.com/artifact/org.springframework/spring-web-client due to RestOperations

### Todos
- JavaDoc

nenaraab: Hi @strehle 
can you please review the API.
Especially this one:
https://github.com/SAP/cloud-security-xsuaa-integration/blob/token-client/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java

Patch:
diff --git a/pom.xml b/pom.xml
index d6e71391..666514b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,6 +61,7 @@
 
 	<modules>
 		<module>api</module>
+		<module>token-client</module>
 		<module>spring-xsuaa</module>
 		<module>spring-xsuaa-it</module>
 		<module>spring-xsuaa-test</module>
diff --git a/samples/spring-security-xsuaa-usage/pom.xml b/samples/spring-security-xsuaa-usage/pom.xml
index b5db23f2..3a3d54bb 100644
--- a/samples/spring-security-xsuaa-usage/pom.xml
+++ b/samples/spring-security-xsuaa-usage/pom.xml
@@ -8,13 +8,13 @@
 	<parent>
 		<groupId>org.springframework.boot</groupId>
 		<artifactId>spring-boot-starter-parent</artifactId>
-		<version>2.1.5.RELEASE</version>
+		<version>2.1.6.RELEASE</version>
 		<relativePath/>
 	</parent>
 
 	<groupId>com.sap.cloud.security.samples</groupId>
 	<artifactId>spring-security-xsuaa-usage</artifactId>
-	<version>1.6.0</version>
+	<version>1.3.0</version>
 	<name>spring-security-xsuaa-usage</name>
 
 	<properties>
@@ -31,7 +31,7 @@
         <dependency>
             <groupId>com.sap.cloud.security.xsuaa</groupId>
             <artifactId>xsuaa-spring-boot-starter</artifactId>
-            <version>${project.version}</version>
+            <version>1.6.0</version>
         </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
diff --git a/samples/vars.yml b/samples/vars.yml
index a30ee4f3..0a3d5f51 100644
--- a/samples/vars.yml
+++ b/samples/vars.yml
@@ -1,7 +1,7 @@
 ---
 # some data to make the urls unique
-# change to another value this if the hostname is already taken
-ID: d048418
+# change to another value, e.g. your User ID
+ID: 00-00-00
 
 # Choose cfapps.eu10.hana.ondemand.com for the EU10 landscape, cfapps.us10.hana.ondemand.com for US10
 LANDSCAPE_APPS_DOMAIN: cfapps.eu10.hana.ondemand.com
diff --git a/spring-xsuaa-mock/README.md b/spring-xsuaa-mock/README.md
index c74b05d0..75c3106d 100644
--- a/spring-xsuaa-mock/README.md
+++ b/spring-xsuaa-mock/README.md
@@ -62,7 +62,7 @@ org.springframework.boot.env.EnvironmentPostProcessor=<<your package>>.XsuaaMock
 
 ### XSUAA Service Configuration
 
-From version `1.5.0` on the [`MockXsuaaServiceConfiguration`](src/main/java/com/sap/cloud/security/xsuaa/mock/MockXsuaaServiceConfiguration.java) is auto-configured. This class overwrites Xsuaa url and uaadomain to point to the Xsuaa Mock Web Server. This is relevant for validating the `jku` URI that is provided as part of the JSON Web Signature (JWS). The `jku` of the Jwt token issued by the `JwtGenerator` references the public key URI of the `XsuaaMockWebServer` used for generating the signature.
+From version `1.5.0` on the [`MockXsuaaServiceConfiguration`](src/main/java/com/sap/cloud/security/xsuaa/mock/MockXsuaaServiceConfiguration.java) is auto-configured [here](src/main/java/com/sap/cloud/security/xsuaa/mock/autoconfiguration/XsuaaMockAutoConfiguration.java). This class overwrites Xsuaa url and uaadomain to point to the Xsuaa Mock Web Server. This is relevant for validating the `jku` URI that is provided as part of the JSON Web Signature (JWS). The `jku` of the Jwt token issued by the `JwtGenerator` references the public key URI of the `XsuaaMockWebServer` used for generating the signature.
 
 ### Extendability
 Note: it is possible to extend the dispatcher and pass this to the `XsuaaMockWebServer` constructor. An example `XsuaaMockPostProcessor` implementation can be found [here](src/test/java/com/sap/cloud/security/xsuaa/mock/XsuaaMockPostProcessor.java).
diff --git a/spring-xsuaa-starter/pom.xml b/spring-xsuaa-starter/pom.xml
index 3d6e6803..a4ce5dcd 100644
--- a/spring-xsuaa-starter/pom.xml
+++ b/spring-xsuaa-starter/pom.xml
@@ -22,7 +22,7 @@
 			<dependency>
 				<groupId>org.springframework.boot</groupId>
 				<artifactId>spring-boot-dependencies</artifactId>
-				<version>2.1.5.RELEASE</version>
+				<version>${spring-boot.version}</version>
 				<type>pom</type>
 				<scope>import</scope>
 			</dependency>
@@ -39,6 +39,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
 		<java.version>1.8</java.version>
+		<spring-boot.version>2.1.5.RELEASE</spring-boot.version>
 	</properties>
 
 	<dependencies>
diff --git a/spring-xsuaa/README.md b/spring-xsuaa/README.md
index 21ac94be..a4da0a30 100644
--- a/spring-xsuaa/README.md
+++ b/spring-xsuaa/README.md
@@ -48,8 +48,8 @@ Then, xsuaa integration libraries auto-configures beans, that are required to in
 
 Auto-configuration class | Description
 ---- | --------
-[XsuaaAutoConfiguration](spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfiguration.java) | Adds `xsuaa.*` properties to Spring's Environment. The properties are by default parsed from `VCAP_SERVICES` system environment variables and can be overwritten by properties such as `xsuaa.xsappname` e.g. for testing purposes. Furthermore it exposes a `XsuaaServiceConfiguration` bean that can be used to access xsuaa service information.  Alternatively you can access them with `@Value` annotation e.g. `@Value("${xsuaa.xsappname:}") String appId`.
-[XsuaaResourceServerJwkAutoConfiguration](spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaResourceServerJwkAutoConfiguration.java) | Configures a `JwtDecoder` bean with a JWK (JSON Web Keys) endpoint from where to download the tenant (subdomain) specific public key.
+[XsuaaAutoConfiguration](/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaAutoConfiguration.java) | Adds `xsuaa.*` properties to Spring's Environment. The properties are by default parsed from `VCAP_SERVICES` system environment variables and can be overwritten by properties such as `xsuaa.xsappname` e.g. for testing purposes. Furthermore it exposes a `XsuaaServiceConfiguration` bean that can be used to access xsuaa service information.  Alternatively you can access them with `@Value` annotation e.g. `@Value("${xsuaa.xsappname:}") String appId`.
+[XsuaaResourceServerJwkAutoConfiguration](/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaResourceServerJwkAutoConfiguration.java) | Configures a `JwtDecoder` bean with a JWK (JSON Web Keys) endpoint from where to download the tenant (subdomain) specific public key.
 
 You can gradually replace auto-configurations as explained [here](https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-auto-configuration.html).
 
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
index be305eec..f5aa0d54 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
@@ -37,8 +37,9 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 	private TokenInfoExtractor tokenInfoExtractor;
 
 	XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidityInSeconds, int cacheSize,
-			OAuth2TokenValidator<Jwt> tokenValidators) {
-		cache = Caffeine.newBuilder().expireAfterWrite(cacheValidityInSeconds, TimeUnit.SECONDS).maximumSize(cacheSize)
+			OAuth2TokenValidator<Jwt> tokenValidators, Collection<PostValidationAction> postValidationActions) {
+
+		this.cache = Caffeine.newBuilder().expireAfterWrite(cacheValidityInSeconds, TimeUnit.SECONDS).maximumSize(cacheSize)
 				.build();
 		this.tokenValidators = tokenValidators;
 
@@ -58,11 +59,6 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 				return xsuaaServiceConfiguration.getUaaDomain();
 			}
 		};
-	}
-
-	XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidityInSeconds, int cacheSize,
-			OAuth2TokenValidator<Jwt> tokenValidators, Collection<PostValidationAction> postValidationActions) {
-		this(xsuaaServiceConfiguration, cacheValidityInSeconds, cacheSize, tokenValidators);
 
 		this.postValidationActions = postValidationActions != null ? postValidationActions : Collections.EMPTY_LIST;
 	}
diff --git a/token-client/pom.xml b/token-client/pom.xml
new file mode 100644
index 00000000..11f31a37
--- /dev/null
+++ b/token-client/pom.xml
@@ -0,0 +1,156 @@
+<?xml version="1.0"?>
+<project
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+	<modelVersion>4.0.0</modelVersion>
+
+	<parent>
+		<groupId>com.sap.cloud.security.xsuaa</groupId>
+		<artifactId>parent</artifactId>
+		<version>1.6.0</version>
+	</parent>
+
+	<artifactId>token-client</artifactId>
+	<name>token-client</name>
+	<packaging>jar</packaging>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.apache.logging.log4j</groupId>
+			<artifactId>log4j-to-slf4j</artifactId>
+			<version>2.11.2</version>
+		</dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <version>${spring.security.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<version>${junit.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.hamcrest</groupId>
+			<artifactId>hamcrest-all</artifactId>
+			<version>1.3</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+			<version>2.6</version>
+			<scope>compile</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-core</artifactId>
+			<version>2.23.0</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.assertj</groupId>
+			<artifactId>assertj-core</artifactId>
+			<version>3.13.2</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+		    <groupId>ch.qos.logback</groupId>
+		    <artifactId>logback-classic</artifactId>
+		    <version>1.2.3</version>
+            <scope>test</scope>
+		</dependency>		
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-javadoc-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>attach-javadocs</id>
+						<goals>
+							<goal>jar</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-source-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>attach-sources</id>
+						<goals>
+							<goal>jar</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.jacoco</groupId>
+				<artifactId>jacoco-maven-plugin</artifactId>
+				<version>0.8.2</version>
+				<executions>
+					<execution>
+						<id>default-prepare-agent</id>
+						<goals>
+							<goal>prepare-agent</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-report</id>
+						<phase>prepare-package</phase>
+						<goals>
+							<goal>report</goal>
+						</goals>
+					</execution>
+					<execution>
+						<id>default-check</id>
+						<goals>
+							<goal>check</goal>
+						</goals>
+						<configuration>
+							<rules>
+							</rules>
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-pmd-plugin</artifactId>
+				<version>3.10.0</version>
+				<executions>
+					<execution>
+						<phase>verify</phase>
+						<goals>
+							<goal>pmd</goal>
+							<goal>cpd</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>findbugs-maven-plugin</artifactId>
+				<version>3.0.5</version>
+				<configuration>
+					<skip>${skipTests}</skip>
+				</configuration>
+				<executions>
+					<execution>
+						<phase>verify</phase>
+						<goals>
+							<goal>findbugs</goal>
+						</goals>
+					</execution>
+				</executions>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/ClientCredentials.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/ClientCredentials.java
new file mode 100644
index 00000000..4a6b19e3
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/ClientCredentials.java
@@ -0,0 +1,43 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import org.springframework.lang.NonNull;
+import org.springframework.util.Assert;
+
+import java.util.Objects;
+
+public class ClientCredentials {
+
+	private final String clientSecret;
+	private final String clientId;
+
+	public ClientCredentials(@NonNull String clientId, @NonNull String clientSecret) {
+		Assert.notNull(clientId, "clientId is required");
+		Assert.notNull(clientSecret, "clientSecret is required");
+		this.clientId = clientId;
+		this.clientSecret = clientSecret;
+	}
+
+	public String getSecret() {
+		return clientSecret;
+	}
+
+	public String getId() {
+		return clientId;
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		if (this == o)
+			return true;
+		if (o == null || getClass() != o.getClass())
+			return false;
+		ClientCredentials that = (ClientCredentials) o;
+		return Objects.equals(clientSecret, that.clientSecret) &&
+				Objects.equals(clientId, that.clientId);
+	}
+
+	@Override
+	public int hashCode() {
+		return Objects.hash(clientSecret, clientId);
+	}
+}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2AccessToken.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2AccessToken.java
new file mode 100644
index 00000000..e23791b7
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2AccessToken.java
@@ -0,0 +1,34 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import java.util.Date;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+public class OAuth2AccessToken {
+	private String refreshToken;
+	private String accessToken;
+	private long expiredTimeMillis;
+
+	public OAuth2AccessToken(String accessToken, long expiredInSeconds, @Nullable String refreshToken) {
+		Assert.hasText(accessToken, "accessToken is required");
+
+		this.accessToken = accessToken;
+		this.expiredTimeMillis = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expiredInSeconds);
+		this.refreshToken = refreshToken;
+	}
+
+	public String getValue() {
+		return accessToken;
+	}
+
+	public Date getExpiredAtDate() {
+		return new Date(expiredTimeMillis);
+	}
+
+	public Optional<String> getRefreshToken() {
+		return Optional.ofNullable(refreshToken);
+	}
+}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceEndpointsProvider.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceEndpointsProvider.java
new file mode 100644
index 00000000..b3b07f38
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceEndpointsProvider.java
@@ -0,0 +1,29 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import java.net.URI;
+
+public interface OAuth2ServiceEndpointsProvider {
+
+	/**
+	 * Returns token endpoint URI.
+	 * 
+	 * @return token endpoint, e.g. {@code https://oauth.server.com/oauth/token}
+	 */
+	URI getTokenEndpoint();
+
+	/**
+	 * Returns authorize endpoint URI.
+	 * 
+	 * @return authorize endpoint, e.g.
+	 *         {@code https://oauth.server.com/oauth/authorize}
+	 */
+	URI getAuthorizeEndpoint();
+
+	/**
+	 * Returns Jwt Key Set URI (JWKS) as specified in
+	 * /.well-known/openid-configuration.
+	 * 
+	 * @return jwks_uri , e.g. {@code https://oauth.server.com/token_keys}
+	 */
+	URI getJwksUri();
+}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceException.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceException.java
new file mode 100644
index 00000000..fa51f225
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2ServiceException.java
@@ -0,0 +1,13 @@
+package com.sap.cloud.security.xsuaa.client;
+
+/**
+ * Exception thrown to signal issues during communication with OAuth2 service.
+ */
+public class OAuth2ServiceException extends RuntimeException {
+
+	private static final long serialVersionUID = 1L;
+
+	public OAuth2ServiceException(String message) {
+		super(message);
+	}
+}
\ No newline at end of file
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java
new file mode 100644
index 00000000..278fc8de
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenService.java
@@ -0,0 +1,86 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import java.net.URI;
+import java.util.Map;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * Retrieves OAuth2 Access Tokens as documented here:
+ * https://docs.cloudfoundry.org/api/uaa/version/4.31.0/index.html#token
+ */
+public interface OAuth2TokenService {
+
+	/**
+	 * Requests access token from OAuth Server with client credentials.
+	 *
+	 * @param tokenEndpointUri
+	 *            the token endpoint URI.
+	 * @param clientCredentials
+	 *            the client id and secret of the OAuth client, the recipient of the
+	 *            token.
+	 * @param subdomain
+	 *            optionally indicates what Identity Zone this request goes to by
+	 *            supplying a subdomain (tenant).
+	 * @param optionalParameters
+	 *            optional request parameters, can be null.
+	 *
+	 * @return the OAuth2AccessToken.
+	 * @throws OAuth2ServiceException
+	 *             in case of an error during the http request.
+	 */
+	OAuth2AccessToken retrieveAccessTokenViaClientCredentialsGrant(URI tokenEndpointUri,
+			ClientCredentials clientCredentials, @Nullable String subdomain,
+			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException;
+
+	/**
+	 * Exchanges user access token from OAuth Server with user access token. This
+	 * endpoint returns only opaque access token, so that another call using {link
+	 * #retrieveAccessTokenViaRefreshToken} is required.
+	 *
+	 * @param tokenEndpointUri
+	 *            the token endpoint URI.
+	 * @param clientCredentials
+	 *            the client id and secret of the OAuth client, the recipient of the
+	 *            token.
+	 * @param token
+	 *            the user bearer token, that represents an authenticated user that
+	 *            must have uaa.user scope.
+	 * @param subdomain
+	 *            optionally indicates what Identity Zone this request goes to by
+	 *            supplying a subdomain (tenant).
+	 * @param optionalParameters
+	 *            optional request parameters, can be null.
+	 *
+	 * @return the OAuth2AccessToken.
+	 * @throws OAuth2ServiceException
+	 *             in case of an error during the http request.
+	 * @deprecated instead use jwt bearer.
+	 */
+	@Deprecated
+	OAuth2AccessToken retrieveAccessTokenViaUserTokenGrant(URI tokenEndpointUri,
+			ClientCredentials clientCredentials, String token, @Nullable String subdomain,
+			@Nullable Map<String, String> optionalParameters)
+			throws OAuth2ServiceException;
+
+	/**
+	 * Requests access token from OAuth Server with refresh-token
+	 *
+	 * @param tokenEndpointUri
+	 *            the token endpoint URI.
+	 * @param clientCredentials
+	 *            the client id and secret of the OAuth client, the recipient of the
+	 *            token.
+	 * @param refreshToken
+	 *            the refresh token that was returned along with the access token
+	 *            {link #OAuth2AccessToken}.
+	 * @param subdomain
+	 *            * optionally indicates what Identity Zone this request goes to by
+	 *            supplying a subdomain (tenant).
+	 * @return the OAuth2AccessToken
+	 * @throws OAuth2ServiceException
+	 *             in case of an error during the http request.
+	 */
+	OAuth2AccessToken retrieveAccessTokenViaRefreshToken(URI tokenEndpointUri, ClientCredentials clientCredentials,
+			String refreshToken, @Nullable String subdomain) throws OAuth2ServiceException;
+}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenServiceConstants.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenServiceConstants.java
new file mode 100644
index 00000000..3fcd5bd4
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenServiceConstants.java
@@ -0,0 +1,22 @@
+package com.sap.cloud.security.xsuaa.client;
+
+public class OAuth2TokenServiceConstants {
+
+	private OAuth2TokenServiceConstants() {
+		throw new IllegalStateException("Utility class");
+	}
+
+	public static final String ACCESS_TOKEN = "access_token";
+	public static final String EXPIRES_IN = "expires_in";
+	public static final String REFRESH_TOKEN = "refresh_token";
+	public static final String CLIENT_ID = "client_id";
+	public static final String CLIENT_SECRET = "client_secret";
+
+	public static final String GRANT_TYPE = "grant_type";
+	public static final String GRANT_TYPE_USER_TOKEN = "user_token";
+	public static final String GRANT_TYPE_REFRESH_TOKEN = "refresh_token";
+	public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
+	public static final String TOKEN_TYPE_OPAQUE = "opaque";
+
+	public static final String PARAMETER_CLIENT_ID = "client_id";
+}
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
new file mode 100644
index 00000000..01ed9802
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpoints.java
@@ -0,0 +1,58 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import java.net.URI;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.util.UriBuilder;
+import org.springframework.web.util.UriComponentsBuilder;
+
+public class XsuaaDefaultEndpoints implements OAuth2ServiceEndpointsProvider {
+	private final URI baseUri;
+	private final static String TOKEN_ENDPOINT = "/oauth/token";
+	private final static String AUTHORIZE_ENDPOINT = "/oauth/authorize";
+	private final static String KEYSET_ENDPOINT = "/token_keys";
+
+	/**
+	 * Creates a new XsuaaRestClient.
+	 *
+	 * @param baseUri
+	 *            - the base URI of XSUAA. Based on the base URI the tokenEndpoint,
+	 *            authorize and key set URI (JWKS) will be derived.
+	 */
+	public XsuaaDefaultEndpoints(URI baseUri) {
+		Assert.notNull(baseUri, "XSUAA base URI must not be null.");
+		this.baseUri = baseUri;
+	}
+
+	/**
+	 * Creates a new XsuaaRestClient.
+	 *
+	 * @param baseUri
+	 *            - the base URI of XSUAA. Based on the base URI the tokenEndpoint,
+	 *            authorize and key set URI (JWKS) will be derived.
+	 */
+	public XsuaaDefaultEndpoints(String baseUri) {
+		this(URI.create(baseUri));
+	}
+
+	@Override
+	public URI getTokenEndpoint() {
+		return UriComponentsBuilder.fromUri(baseUri).path(TOKEN_ENDPOINT).build().toUri();
+	}
+
+	@Override
+	public URI getAuthorizeEndpoint() {
+		return UriComponentsBuilder.fromUri(baseUri).path(AUTHORIZE_ENDPOINT).build().toUri();
+	}
+
+	@Override
+	public URI getJwksUri() {
+		return UriComponentsBuilder.fromUri(baseUri).path(KEYSET_ENDPOINT).build().toUri();
+	}
+
+}
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
new file mode 100644
index 00000000..9201cd2d
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenService.java
@@ -0,0 +1,214 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.*;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestOperations;
+import org.springframework.web.util.UriBuilder;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.*;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.GRANT_TYPE;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.GRANT_TYPE_CLIENT_CREDENTIALS;
+
+public class XsuaaOAuth2TokenService implements OAuth2TokenService {
+
+	private RestOperations restOperations;
+	private static Logger logger = LoggerFactory.getLogger(XsuaaOAuth2TokenService.class);
+
+	public XsuaaOAuth2TokenService(@NonNull RestOperations restOperations) {
+		Assert.notNull(restOperations, "restOperations is required");
+		this.restOperations = restOperations;
+	}
+
+	@Override
+	public OAuth2AccessToken retrieveAccessTokenViaClientCredentialsGrant(@NonNull URI tokenEndpointUri,
+			@NonNull ClientCredentials clientCredentials,
+			@Nullable String subdomain, @Nullable Map<String, String> optionalParameters)
+			throws OAuth2ServiceException {
+		Assert.notNull(tokenEndpointUri, "tokenEndpointUri is required");
+		Assert.notNull(clientCredentials, "clientCredentials is required");
+
+		Map<String, String> parameters = copy(optionalParameters);
+		parameters.put(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS);
+		parameters.put(CLIENT_ID, clientCredentials.getId());
+		parameters.put(CLIENT_SECRET, clientCredentials.getSecret());
+
+		// build header
+		HttpHeaders headers = createHeadersWithoutAuthorization();
+
+		return requestAccessToken(replaceSubdomain(tokenEndpointUri, subdomain), headers, parameters);
+	}
+
+	@Override
+	public OAuth2AccessToken retrieveAccessTokenViaUserTokenGrant(@NonNull URI tokenEndpointUri,
+			@NonNull ClientCredentials clientCredentials, @NonNull String token, @Nullable String subdomain,
+			@Nullable Map<String, String> optionalParameters)
+			throws OAuth2ServiceException {
+		Assert.notNull(tokenEndpointUri, "tokenEndpointUri is required");
+		Assert.notNull(clientCredentials, "clientCredentials is required");
+		Assert.notNull(token, "token is required");
+
+		Map<String, String> parameters = copy(optionalParameters);
+		parameters.put(GRANT_TYPE, GRANT_TYPE_USER_TOKEN);
+		parameters.put(PARAMETER_CLIENT_ID, clientCredentials.getId());
+
+		// build header
+		HttpHeaders headers = createHeadersWithAuthorization(token);
+
+		return requestAccessToken(replaceSubdomain(tokenEndpointUri, subdomain), headers, parameters);
+	}
+
+	@Override
+	public OAuth2AccessToken retrieveAccessTokenViaRefreshToken(@NonNull URI tokenEndpointUri,
+			@NonNull ClientCredentials clientCredentials,
+			@NonNull String refreshToken, String subdomain) throws OAuth2ServiceException {
+		Assert.notNull(tokenEndpointUri, "tokenEndpointUri is required");
+		Assert.notNull(clientCredentials, "clientCredentials is required");
+		Assert.notNull(refreshToken, "refreshToken is required");
+
+		Map<String, String> parameters = new HashMap<>();
+		parameters.put(GRANT_TYPE, GRANT_TYPE_REFRESH_TOKEN);
+		parameters.put(REFRESH_TOKEN, refreshToken);
+		parameters.put(CLIENT_ID, clientCredentials.getId());
+		parameters.put(CLIENT_SECRET, clientCredentials.getSecret());
+
+		// build header
+		HttpHeaders headers = createHeadersWithoutAuthorization();
+
+		return requestAccessToken(replaceSubdomain(tokenEndpointUri, subdomain), headers, parameters);
+	}
+
+	/**
+	 * Utility method that replaces the subdomain of the URI with the given
+	 * subdomain.
+	 *
+	 * @param uri
+	 *            the URI to be replaced.
+	 * @param subdomain
+	 *            of the tenant.
+	 * @return the URI with the replaced subdomain or the passed URI in case a
+	 *         replacement was not possible.
+	 */
+	static URI replaceSubdomain(@NonNull URI uri, @Nullable String subdomain) {
+		Assert.notNull(uri, "the uri parameter must not be null");
+		if (StringUtils.hasText(subdomain) && uri.getHost().contains(".")) {
+			UriBuilder builder = UriComponentsBuilder.newInstance().scheme(uri.getScheme())
+					.host(subdomain + uri.getHost().substring(uri.getHost().indexOf("."))).port(uri.getPort())
+					.path(uri.getPath());
+			return uri.resolve(builder.build());
+		}
+		logger.warn("the subdomain of the URI '{}' is not replaced by subdomain '{}'", uri, subdomain);
+		return uri;
+	}
+
+	private OAuth2AccessToken requestAccessToken(URI tokenEndpointUri, HttpHeaders headers,
+			Map<String, String> parameters) {
+
+		// Create URI
+		UriComponentsBuilder builder = UriComponentsBuilder.fromUri(tokenEndpointUri);
+		parameters.forEach(builder::queryParam);
+		URI requestUri = builder.build().encode().toUri();
+
+		// Create entity
+		HttpEntity<Void> requestEntity = new HttpEntity<>(headers);
+
+		@SuppressWarnings("rawtypes")
+		ResponseEntity<Map> responseEntity = null;
+		try {
+			responseEntity = restOperations.postForEntity(requestUri, requestEntity, Map.class);
+		} catch (HttpClientErrorException ex) {
+			HttpStatus responseStatusCode = ex.getStatusCode();
+			if (responseStatusCode == HttpStatus.UNAUTHORIZED) {
+				throw new OAuth2ServiceException(String.format(
+						"Error retrieving JWT token. Received status code %s. Call to XSUAA was not successful. Client credentials invalid.",
+						responseStatusCode));
+			}
+			if (!responseStatusCode.is2xxSuccessful()) {
+				throw new OAuth2ServiceException(String.format(
+						"Error retrieving JWT token. Received status code %s. Call to XSUAA was not successful.",
+						responseStatusCode));
+			}
+			throw new OAuth2ServiceException(String.format(
+					"Error retrieving JWT token. Call to XSUAA was not successful: %s",
+					ex.getMessage()));
+		}
+
+		@SuppressWarnings("unchecked")
+		Map<String, String> accessTokenMap = responseEntity.getBody();
+
+		String accessToken = accessTokenMap.get(ACCESS_TOKEN);
+		long expiresIn = Long.parseLong(String.valueOf(accessTokenMap.get(EXPIRES_IN)));
+		String refreshToken = accessTokenMap.get(REFRESH_TOKEN);
+		return new OAuth2AccessToken(accessToken, expiresIn, refreshToken);
+	}
+
+	/**
+	 * Create a copy of the given map or an new empty map
+	 * 
+	 * @return a new Map that contains all entries of the optional map
+	 */
+	private static Map<String, String> copy(Map<String, String> map) {
+		return map == null ? new HashMap<>() : new HashMap<>(map);
+	}
+
+	/**
+	 * Creates the set of HTTP headers with client-credentials basic authentication
+	 * header.
+	 *
+	 * @return the HTTP headers.
+	 */
+	private static HttpHeaders createHeadersWithoutAuthorization() {
+		HttpHeaders headers = new HttpHeaders();
+		addAcceptHeader(headers);
+		return headers;
+	}
+
+	/**
+	 * Creates the set of HTTP headers with Authorization Bearer header.
+	 *
+	 * @return the HTTP headers.
+	 */
+	private static HttpHeaders createHeadersWithAuthorization(String token) {
+		HttpHeaders headers = new HttpHeaders();
+		addAcceptHeader(headers);
+		addAuthorizationBearerHeader(headers, token);
+		return headers;
+	}
+
+	/** common utilities **/
+
+	/**
+	 * Adds the {@code  Accept: application/json} header to the set of headers.
+	 *
+	 * @param headers
+	 *            - the set of headers to add the header to.
+	 */
+	static void addAcceptHeader(HttpHeaders headers) {
+		headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
+	}
+
+	/**
+	 * Adds the {@code  Authorization: Bearer <token>} header to the set of headers.
+	 *
+	 * @param headers
+	 *            - the set of headers to add the header to.
+	 * @param token
+	 *            - the token which should be part of the header.
+	 */
+	static void addAuthorizationBearerHeader(HttpHeaders headers, String token) {
+		final String AUTHORIZATION_BEARER_TOKEN_FORMAT = "Bearer %s";
+		headers.add(HttpHeaders.AUTHORIZATION, String.format(AUTHORIZATION_BEARER_TOKEN_FORMAT, token));
+	}
+}


Test patch:
diff --git a/spring-xsuaa-it/src/main/java/testservice/api/basic/SecurityConfiguration.java b/spring-xsuaa-it/src/main/java/testservice/api/basic/SecurityConfiguration.java
index 2a4f8ea7..c21906f0 100644
--- a/spring-xsuaa-it/src/main/java/testservice/api/basic/SecurityConfiguration.java
+++ b/spring-xsuaa-it/src/main/java/testservice/api/basic/SecurityConfiguration.java
@@ -15,12 +15,6 @@
  */
 package testservice.api.basic;
 
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
-import com.sap.cloud.security.xsuaa.extractor.AuthenticationMethod;
-import com.sap.cloud.security.xsuaa.extractor.TokenBrokerResolver;
-import com.sap.cloud.security.xsuaa.mock.MockXsuaaServiceConfiguration;
-import com.sap.cloud.security.xsuaa.token.TokenAuthenticationConverter;
-import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.cache.CacheManager;
 import org.springframework.cache.annotation.EnableCaching;
@@ -34,6 +28,13 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.extractor.AuthenticationMethod;
+import com.sap.cloud.security.xsuaa.extractor.TokenBrokerResolver;
+import com.sap.cloud.security.xsuaa.mock.MockXsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.token.TokenAuthenticationConverter;
+import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder;
+
 @EnableWebSecurity
 @EnableCaching
 @Profile({ "test.api.basic" })
diff --git a/spring-xsuaa-it/src/main/java/testservice/api/basic/TestController.java b/spring-xsuaa-it/src/main/java/testservice/api/basic/TestController.java
index 261cf4ef..67b37fc2 100644
--- a/spring-xsuaa-it/src/main/java/testservice/api/basic/TestController.java
+++ b/spring-xsuaa-it/src/main/java/testservice/api/basic/TestController.java
@@ -25,7 +25,6 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Profile;
 import org.springframework.security.core.GrantedAuthority;
@@ -35,6 +34,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
 import com.sap.cloud.security.xsuaa.token.Token;
 import com.sap.xs2.security.container.XSTokenRequestImpl;
 import com.sap.xsa.security.container.XSTokenRequest;
diff --git a/spring-xsuaa-it/src/main/java/testservice/api/v1/SecurityConfiguration.java b/spring-xsuaa-it/src/main/java/testservice/api/v1/SecurityConfiguration.java
index 41f458b2..97ce4c31 100644
--- a/spring-xsuaa-it/src/main/java/testservice/api/v1/SecurityConfiguration.java
+++ b/spring-xsuaa-it/src/main/java/testservice/api/v1/SecurityConfiguration.java
@@ -15,7 +15,6 @@
  */
 package testservice.api.v1;
 
-import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Profile;
 import org.springframework.core.convert.converter.Converter;
@@ -24,11 +23,12 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
 import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
 import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
 
 import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
 import com.sap.cloud.security.xsuaa.mock.MockXsuaaServiceConfiguration;
 import com.sap.cloud.security.xsuaa.token.TokenAuthenticationConverter;
-import org.springframework.security.oauth2.jwt.JwtDecoder;
+import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder;
 
 @EnableWebSecurity
 @Profile({ "test.api.v1" })
diff --git a/spring-xsuaa-it/src/main/java/testservice/api/v1/TestController.java b/spring-xsuaa-it/src/main/java/testservice/api/v1/TestController.java
index e41e32dd..958afbc8 100644
--- a/spring-xsuaa-it/src/main/java/testservice/api/v1/TestController.java
+++ b/spring-xsuaa-it/src/main/java/testservice/api/v1/TestController.java
@@ -8,7 +8,6 @@ import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
 import org.junit.Assert;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Profile;
@@ -19,6 +18,7 @@ import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
 import com.sap.cloud.security.xsuaa.token.Token;
 import com.sap.xs2.security.container.XSTokenRequestImpl;
 import com.sap.xsa.security.container.XSTokenRequest;
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/ClientCredentialsTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/ClientCredentialsTest.java
new file mode 100644
index 00000000..8bdd7963
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/ClientCredentialsTest.java
@@ -0,0 +1,24 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+
+public class ClientCredentialsTest {
+	ClientCredentials cut = new ClientCredentials("clientId", "clientSecret");
+
+	@Test
+	public void clientCredentials_equals() {
+		assertThat(cut.equals(cut), is(true));
+		assertThat(cut.equals(new ClientCredentials("clientId", "clientSecret")), is(true));
+	}
+
+	@Test
+	public void clientCredentials_notequals() {
+		assertThat(cut.equals(new ClientCredentials("clientId2", "clientSecret")), is(false));
+		assertThat(cut.equals(new ClientCredentials("clientId", "clientSecret2")), is(false));
+		assertThat(cut.equals(null), is(false));
+		assertThat(cut.equals(new Object()), is(false));
+	}
+}
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/OAuth2AccessTokenTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/OAuth2AccessTokenTest.java
new file mode 100644
index 00000000..eed0d373
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/OAuth2AccessTokenTest.java
@@ -0,0 +1,25 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.junit.Assert.assertThat;
+
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import org.hamcrest.number.OrderingComparison;
+import org.junit.Test;
+
+public class OAuth2AccessTokenTest {
+
+	@Test
+	public void getExpiredDateFromAccessToken() {
+		long expireInSeconds = 43199;
+		Date minExpireDate = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expireInSeconds));
+		OAuth2AccessToken accessToken = new OAuth2AccessToken("e9511922b5e64c49ba0eedcc8d772e76", expireInSeconds,
+				null);
+		Date maxExpireDate = new Date(System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(expireInSeconds));
+
+		assertThat(accessToken.getExpiredAtDate(), allOf(OrderingComparison.greaterThanOrEqualTo(minExpireDate),
+				OrderingComparison.lessThanOrEqualTo(maxExpireDate)));
+	}
+}
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
new file mode 100644
index 00000000..efa15960
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaDefaultEndpointsTest.java
@@ -0,0 +1,28 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import org.junit.Test;
+
+import java.net.URI;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+public class XsuaaDefaultEndpointsTest {
+	XsuaaDefaultEndpoints cut = new XsuaaDefaultEndpoints("https://subdomain.myauth.com");
+
+	@Test
+	public void getTokenEndpoint() {
+		assertThat(cut.getTokenEndpoint().toString(), is("https://subdomain.myauth.com/oauth/token"));
+	}
+
+	@Test
+	public void getAuthorizeEndpoint() {
+		assertThat(cut.getAuthorizeEndpoint().toString(), is("https://subdomain.myauth.com/oauth/authorize"));
+	}
+
+	@Test
+	public void getJwksUri() {
+		assertThat(cut.getJwksUri().toString(), is("https://subdomain.myauth.com/token_keys"));
+	}
+
+}
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceClientCredentialsTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceClientCredentialsTest.java
new file mode 100644
index 00000000..ffdfa95c
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceClientCredentialsTest.java
@@ -0,0 +1,142 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.hamcrest.CoreMatchers.is;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestOperations;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.ACCESS_TOKEN;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.any;
+
+@RunWith(MockitoJUnitRunner.class)
+public class XsuaaOAuth2TokenServiceClientCredentialsTest {
+
+	OAuth2TokenService cut;
+	ClientCredentials clientCredentials;
+	URI tokenEndpoint;
+	Map<String, String> responseMap;
+
+	@Mock
+	RestOperations mockRestOperations;
+
+	@Before
+	public void setup() {
+		cut = new XsuaaOAuth2TokenService(mockRestOperations);
+		clientCredentials = new ClientCredentials("clientid", "mysecretpassword");
+		tokenEndpoint = URI.create("https://subdomain.myauth.server.com/oauth/token");
+
+		responseMap = new HashMap<>();
+		responseMap.putIfAbsent(ACCESS_TOKEN, "f529.dd6e30.d454677322aaabb0");
+		responseMap.putIfAbsent(OAuth2TokenServiceConstants.EXPIRES_IN, "43199");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void initialize_throwsIfRestOperationsIsNull() {
+		new XsuaaOAuth2TokenService(null);
+	}
+
+	@Test
+	public void retrieveToken_throwsOnNullValues() {
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaClientCredentialsGrant(null, clientCredentials, null, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("tokenEndpointUri");
+
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint, null, null, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientCredentials");
+	}
+
+	@Test(expected = OAuth2ServiceException.class)
+	public void retrieveToken_throwsIfHttpStatusUnauthorized() {
+		Mockito.when(mockRestOperations.postForEntity(any(URI.class), any(HttpEntity.class), eq(Map.class)))
+				.thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED));
+		cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint, clientCredentials,
+				null, null);
+	}
+
+	@Test(expected = OAuth2ServiceException.class)
+	public void retrieveToken_throwsIfHttpStatusNotOk() {
+		Mockito.when(mockRestOperations.postForEntity(any(URI.class), any(HttpEntity.class), eq(Map.class)))
+				.thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST));
+		cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint, clientCredentials,
+				null, null);
+	}
+
+	@Test
+	public void retrieveToken() {
+		HttpHeaders expectedHeaders = new HttpHeaders();
+		expectedHeaders.add(HttpHeaders.ACCEPT, "application/json");
+		HttpEntity expectedRequest = new HttpEntity(expectedHeaders);
+
+		Mockito.when(mockRestOperations
+				.postForEntity(
+						eq(createUriWithParameters(
+								"grant_type=client_credentials&client_secret=mysecretpassword&client_id=clientid")),
+						eq(expectedRequest),
+						eq(Map.class)))
+				.thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		OAuth2AccessToken accessToken = cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint,
+				clientCredentials,
+				null, null);
+		assertThat(accessToken.getValue(), is(responseMap.get(ACCESS_TOKEN)));
+		assertNotNull(accessToken.getExpiredAtDate());
+	}
+
+	@Test
+	public void retrieveToken_withOptionalParamaters() {
+		Mockito.when(mockRestOperations.postForEntity(
+				eq(createUriWithParameters(
+						"add-param-1=value1&add-param-2=value2&client_secret=mysecretpassword&grant_type=client_credentials&client_id=clientid")),
+				any(HttpEntity.class), eq(Map.class)))
+				.thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		Map<String, String> additionalParameters = new HashMap<>();
+		additionalParameters.put("add-param-1", "value1");
+		additionalParameters.put("add-param-2", "value2");
+
+		OAuth2AccessToken accessToken = cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint,
+				clientCredentials, null,
+				additionalParameters);
+		assertThat(accessToken.getValue(), is(responseMap.get(ACCESS_TOKEN)));
+	}
+
+	@Test
+	public void retrieveToken_requiredParametersCanNotBeOverwritten() {
+		Mockito.when(mockRestOperations.postForEntity(
+				eq(createUriWithParameters(
+						"grant_type=client_credentials&client_id=clientid&client_secret=mysecretpassword")),
+				any(HttpEntity.class), eq(Map.class)))
+				.thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		Map<String, String> overwrittenGrantType = new HashMap<>();
+		overwrittenGrantType.put(OAuth2TokenServiceConstants.GRANT_TYPE, "overwrite-obligatory-param");
+
+		OAuth2AccessToken accessToken = cut.retrieveAccessTokenViaClientCredentialsGrant(tokenEndpoint,
+				clientCredentials, null,
+				overwrittenGrantType);
+		assertThat(accessToken.getValue(), is(responseMap.get(ACCESS_TOKEN)));
+	}
+
+	private URI createUriWithParameters(String queryParameterList) {
+		return URI.create(tokenEndpoint.toString() + "?" + queryParameterList);
+	}
+}
\ No newline at end of file
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceRefreshTokenTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceRefreshTokenTest.java
new file mode 100644
index 00000000..b7af6240
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceRefreshTokenTest.java
@@ -0,0 +1,116 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestOperations;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.ACCESS_TOKEN;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.REFRESH_TOKEN;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.EXPIRES_IN;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+
+@RunWith(MockitoJUnitRunner.class)
+public class XsuaaOAuth2TokenServiceRefreshTokenTest {
+
+	private static String refreshToken = "d2faefe7ea834ba895d20730f106128c-r";
+
+	OAuth2TokenService cut;
+	ClientCredentials clientCredentials;
+	URI tokenEndpoint;
+	Map<String, String> responseMap;
+
+	@Mock
+	RestOperations mockRestOperations;
+
+	@Before
+	public void setup() {
+		cut = new XsuaaOAuth2TokenService(mockRestOperations);
+		clientCredentials = new ClientCredentials("clientid", "mysecretpassword");
+		tokenEndpoint = URI.create("https://subdomain.myauth.server.com/oauth/token");
+
+		responseMap = new HashMap<>();
+		responseMap.put(REFRESH_TOKEN, "2170b564228448c6aed8b1ddfdb8bf53-r");
+		responseMap.put(ACCESS_TOKEN, "f529.dd6e30.d454677322aaabb0");
+		responseMap.put(EXPIRES_IN, "43199");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void initialize_throwsIfRestOperationsIsNull() {
+		new XsuaaOAuth2TokenService(null);
+	}
+
+	@Test
+	public void retrieveToken_throwsOnNullValues() {
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaRefreshToken(null, clientCredentials, refreshToken, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("tokenEndpointUri");
+
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaRefreshToken(tokenEndpoint, null, refreshToken, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientCredentials");
+
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaRefreshToken(tokenEndpoint, clientCredentials, null, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("refreshToken");
+	}
+
+	@Test(expected = OAuth2ServiceException.class)
+	public void retrieveToken_throwsIfHttpStatusUnauthorized() {
+		Mockito.when(mockRestOperations.postForEntity(any(URI.class), any(HttpEntity.class), eq(Map.class)))
+				.thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED));
+		cut.retrieveAccessTokenViaRefreshToken(tokenEndpoint, clientCredentials,
+				refreshToken, null);
+	}
+
+	@Test(expected = OAuth2ServiceException.class)
+	public void retrieveToken_throwsIfHttpStatusNotOk() {
+		Mockito.when(mockRestOperations.postForEntity(any(URI.class), any(HttpEntity.class), eq(Map.class)))
+				.thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST));
+		cut.retrieveAccessTokenViaRefreshToken(tokenEndpoint, clientCredentials,
+				refreshToken, null);
+	}
+
+	@Test
+	public void retrieveToken() {
+		HttpHeaders expectedHeaders = new HttpHeaders();
+		expectedHeaders.add(HttpHeaders.ACCEPT, "application/json");
+		HttpEntity expectedRequest = new HttpEntity(expectedHeaders);
+
+		Mockito.when(mockRestOperations
+				.postForEntity(
+						eq(createUriWithParameters(
+								"refresh_token=" + refreshToken
+										+ "&grant_type=refresh_token&client_secret=mysecretpassword&client_id=clientid")),
+						eq(expectedRequest),
+						eq(Map.class)))
+				.thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		OAuth2AccessToken accessToken = cut.retrieveAccessTokenViaRefreshToken(tokenEndpoint, clientCredentials,
+				refreshToken, null);
+		assertThat(accessToken.getRefreshToken().get(), is(responseMap.get(REFRESH_TOKEN)));
+		assertThat(accessToken.getValue(), is(responseMap.get(ACCESS_TOKEN)));
+		assertNotNull(accessToken.getExpiredAtDate());
+	}
+
+	private URI createUriWithParameters(String queryParameterList) {
+		return URI.create(tokenEndpoint.toString() + "?" + queryParameterList);
+	}
+}
\ No newline at end of file
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceTest.java
new file mode 100644
index 00000000..8aa3575d
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceTest.java
@@ -0,0 +1,38 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.net.URI;
+
+import org.junit.Test;
+import org.springframework.web.client.RestTemplate;
+
+public class XsuaaOAuth2TokenServiceTest {
+	private XsuaaOAuth2TokenService cut = new XsuaaOAuth2TokenService(new RestTemplate());
+	private URI tokenEndpointUri = URI.create("https://subdomain.myauth.com/mypath");
+
+	@Test
+	public void replaceSubdomain_replacesNothingWhenSubdomainIsNull() {
+		URI replacedURI = XsuaaOAuth2TokenService.replaceSubdomain(tokenEndpointUri, null);
+		assertThat(replacedURI, is(tokenEndpointUri));
+	}
+
+	@Test
+	public void replaceSubdomain() {
+		URI replacedURI = XsuaaOAuth2TokenService.replaceSubdomain(tokenEndpointUri, "newsubdomain");
+		assertThat(replacedURI.toString(), is("https://newsubdomain.myauth.com/mypath"));
+	}
+
+	@Test
+	public void replaceSubdomain_replacesNothingWhenSubdomainIsEmpty() {
+		URI replacedURI = XsuaaOAuth2TokenService.replaceSubdomain(tokenEndpointUri, "");
+		assertThat(replacedURI, is(tokenEndpointUri));
+	}
+
+	@Test
+	public void replaceSubdomain_replacesNothingWhenUrlContainsNoSubdomain() {
+		URI replacedURI = XsuaaOAuth2TokenService.replaceSubdomain(URI.create("http://localhost"), "newsubdomain");
+		assertThat(replacedURI.toString(), is("http://localhost"));
+	}
+}
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceUserTokenTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceUserTokenTest.java
new file mode 100644
index 00000000..7a372d60
--- /dev/null
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/XsuaaOAuth2TokenServiceUserTokenTest.java
@@ -0,0 +1,146 @@
+package com.sap.cloud.security.xsuaa.client;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestOperations;
+
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.ACCESS_TOKEN;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.REFRESH_TOKEN;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.EXPIRES_IN;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.eq;
+
+@RunWith(MockitoJUnitRunner.class)
+public class XsuaaOAuth2TokenServiceUserTokenTest {
+
+	OAuth2TokenService cut;
+	ClientCredentials clientCredentials;
+	URI tokenEndpoint;
+	Map<String, String> responseMap;
+	private static final String userTokenToBeExchanged = "65a84cd45c554c6993ea26cb8f9cf3a2";
+
+	@Mock
+	RestOperations mockRestOperations;
+
+	@Before
+	public void setup() {
+		cut = new XsuaaOAuth2TokenService(mockRestOperations);
+		clientCredentials = new ClientCredentials("clientid", "mysecretpassword");
+		tokenEndpoint = URI.create("https://subdomain.myauth.server.com/oauth/token");
+
+		responseMap = new HashMap<>();
+		responseMap.put(REFRESH_TOKEN, "2170b564228448c6aed8b1ddfdb8bf53-r");
+		responseMap.put(ACCESS_TOKEN, "4d841646fcc340f59b1b7b43df4b050d"); // opaque access token
+		responseMap.put(EXPIRES_IN, "43199");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void initialize_throwsIfRestOperationsIsNull() {
+		new XsuaaOAuth2TokenService(null);
+	}
+
+	@Test
+	public void retrieveToken_throwsOnNullValues() {
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaUserTokenGrant(null, clientCredentials, userTokenToBeExchanged, null, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("tokenEndpointUri");
+
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaUserTokenGrant(tokenEndpoint, null, userTokenToBeExchanged, null, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientCredentials");
+
+		assertThatThrownBy(() -> {
+			cut.retrieveAccessTokenViaUserTokenGrant(tokenEndpoint, clientCredentials, null, null, null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("token");
+	}
+
+	@Test(expected = OAuth2ServiceException.class)
+	public void retrieveToken_throwsIfHttpStatusUnauthorized() {
+		Mockito.when(mockRestOperations.postForEntity(any(URI.class), any(HttpEntity.class), eq(Map.class)))
+				.thenThrow(new HttpClientErrorException(HttpStatus.UNAUTHORIZED));
+		cut.retrieveAccessTokenViaUserTokenGrant(tokenEndpoint, clientCredentials,
+				userTokenToBeExchanged, null, null);
+	}
+
+	@Test(expected = OAuth2ServiceException.class)
+	public void retrieveToken_throwsIfHttpStatusNotOk() {
+		Mockito.when(mockRestOperations.postForEntity(any(URI.class), any(HttpEntity.class), eq(Map.class)))
+				.thenThrow(new HttpClientErrorException(HttpStatus.BAD_REQUEST));
+		cut.retrieveAccessTokenViaUserTokenGrant(tokenEndpoint, clientCredentials,
+				userTokenToBeExchanged, null, null);
+	}
+
+	@Test
+	public void retrieveToken() {
+		HttpHeaders expectedHeaders = new HttpHeaders();
+		expectedHeaders.add(HttpHeaders.ACCEPT, "application/json");
+		expectedHeaders.add(HttpHeaders.AUTHORIZATION, "Bearer " + userTokenToBeExchanged);
+		HttpEntity expectedRequest = new HttpEntity(expectedHeaders);
+
+		Mockito.when(mockRestOperations
+				.postForEntity(eq(createUriWithParameters("grant_type=user_token&client_id=clientid")),
+						eq(expectedRequest),
+						eq(Map.class)))
+				.thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		OAuth2AccessToken accessToken = cut.retrieveAccessTokenViaUserTokenGrant(tokenEndpoint, clientCredentials,
+				userTokenToBeExchanged, null, null);
+		assertThat(accessToken.getRefreshToken().get(), is(responseMap.get(REFRESH_TOKEN)));
+		assertThat(accessToken.getValue(), is(responseMap.get(ACCESS_TOKEN)));
+		assertNotNull(accessToken.getExpiredAtDate());
+	}
+
+	@Test
+	public void retrieveToken_withOptionalParamaters() {
+		Mockito.when(mockRestOperations.postForEntity(
+				eq(createUriWithParameters(
+						"add-param-1=value1&add-param-2=value2&grant_type=user_token&client_id=clientid")),
+				any(HttpEntity.class), eq(Map.class)))
+				.thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		Map<String, String> additionalParameters = new HashMap<>();
+		additionalParameters.put("add-param-1", "value1");
+		additionalParameters.put("add-param-2", "value2");
+
+		OAuth2AccessToken accessToken = cut.retrieveAccessTokenViaUserTokenGrant(tokenEndpoint, clientCredentials,
+				userTokenToBeExchanged, null, additionalParameters);
+		assertThat(accessToken.getRefreshToken().get(), is(responseMap.get(REFRESH_TOKEN)));
+	}
+
+	@Test
+	public void retrieveToken_requiredParametersCanNotBeOverwritten() {
+		Mockito.when(
+				mockRestOperations.postForEntity(
+						eq(createUriWithParameters("grant_type=user_token&client_id=clientid")),
+						any(HttpEntity.class), eq(Map.class)))
+				.thenReturn(new ResponseEntity<>(responseMap, HttpStatus.OK));
+
+		Map<String, String> overwrittenGrantType = new HashMap<>();
+		overwrittenGrantType.put(OAuth2TokenServiceConstants.GRANT_TYPE, "overwrite-obligatory-param");
+
+		OAuth2AccessToken accessToken = cut.retrieveAccessTokenViaUserTokenGrant(tokenEndpoint, clientCredentials,
+				userTokenToBeExchanged, null, overwrittenGrantType);
+		assertThat(accessToken.getRefreshToken().get(), is(responseMap.get(REFRESH_TOKEN)));
+	}
+
+	private URI createUriWithParameters(String queryParameterList) {
+		return URI.create(tokenEndpoint.toString() + "?" + queryParameterList);
+	}
+}
\ No newline at end of file