Repository: SAP/cloud-security-xsuaa-integration
Pull request #281 (2020-05-12 14:16:12)

Token client cache per request
This PR adds the functionality to disable the token cache per request. This also includes refactoring of token flow tests.
This PR is based on the branch of #280 

hassler-d: Implemented requirement from #152 

> return cached token only in case token isNotExpired

In commit [416d39e](https://github.com/SAP/cloud-security-xsuaa-integration/pull/281/commits/416d39eb5f98e9195fc894dc06012c0f4c8296e6)

Patch:
diff --git a/token-client/README.md b/token-client/README.md
index fcdc9a31..fe315a04 100644
--- a/token-client/README.md
+++ b/token-client/README.md
@@ -45,6 +45,11 @@ The `DefaultOAuth2TokenService` can also be instantiated with a custom `Closeabl
 
 > The `<uaa_base_url>`, `<client_id>` and `<client_secret>` are placeholders for the information you get from the XSUAA service binding. 
 
+##### Cache
+
+By default, the `DefaultOAuth2TokenService` caches tokens internally. The Cache can be configured by providing an
+`CacheConfiguration` object as constructor parameter. The cache can be disabled by using the
+`CacheConfiguration.CACHE_DISABLED` configuration.
 
 ## Configuration for Java/Spring Applications
 
@@ -72,6 +77,12 @@ XsuaaTokenFlows tokenFlows = new XsuaaTokenFlows(
 ```
 > The `<uaa_base_url>`, `<client_id>` and `<client_secret>` are placeholders for the information you get from the XSUAA service binding. In case you leverage the spring-xsuaa library you can also use [`XsuaaServiceConfiguration`](/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServiceConfiguration.java) class.
 
+##### Cache
+
+By default, the `XsuaaOAuth2TokenService` caches tokens internally. The Cache can be configured by providing an
+`CacheConfiguration` object as constructor parameter. The cache can be disabled by using the
+`CacheConfiguration.CACHE_DISABLED` configuration.
+
 ## Configuration for Spring Boot Applications
 
 #### Maven Dependencies
@@ -113,6 +124,7 @@ Obtain a client credentials token:
 ```java
 OAuth2TokenResponse clientCredentialsToken = tokenFlows.clientCredentialsTokenFlow()
                                                     .subdomain(jwtToken.getSubdomain()) // this is optional 
+                                                    .disableCache(true)                 // optionally disables token cache for request
                                                     .execute();
 ```
 ### Refresh Token Flow
@@ -122,6 +134,7 @@ In case you have a refresh token and want to obtain an access token:
 OAuth2TokenResponse refreshToken = tokenFlows.refreshTokenFlow()
                               .refreshToken(<refresh_token>)
                               .subdomain(jwtToken.getSubdomain()) // this is optional 
+                              .disableCache(true)                 // optionally disables token cache for request
                               .execute();
 ```
 ### User Token Flow
@@ -132,7 +145,8 @@ XsuaaToken jwtToken = SpringSecurityContext.getToken();
 OAuth2TokenResponse userToken = tokenFlows.userTokenFlow()
                 .token(jwtToken)
                 .subdomain(jwtToken.getSubdomain()) // this is optional      
-                .attributes(additionalAttributes) // this is optional
+                .disableCache(true)                 // optionally disables token cache for request
+                .attributes(additionalAttributes)   // this is optional
                 .execute();
 ```
 
@@ -142,6 +156,7 @@ In order to obtain an access token for a user:
 ```java
 OAuth2TokenResponse clientCredentialsToken = tokenFlows.passwordTokenFlow()
                                                     .subdomain(jwtToken.getSubdomain()) // this is optional
+                                                    .disableCache(true)                 // optionally disables token cache for request
                                                     .username(<your username>)
                                                     .password(<your password>)
                                                     .execute();
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenService.java
index 89a9b53f..40f4bc8d 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenService.java
@@ -13,6 +13,9 @@ import com.sap.cloud.security.xsuaa.util.UriUtil;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.net.URI;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Objects;
 
@@ -26,7 +29,7 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 	private final CacheConfiguration cacheConfiguration;
 
 	public AbstractOAuth2TokenService() {
-		this(Ticker.systemTicker(), false, CacheConfiguration.DEFAULT);
+		this(CacheConfiguration.DEFAULT, Ticker.systemTicker(), false);
 	}
 
 	/**
@@ -36,23 +39,23 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 	 *            the cache configuration used to configure the cache.
 	 */
 	public AbstractOAuth2TokenService(CacheConfiguration cacheConfiguration) {
-		this(Ticker.systemTicker(), false, cacheConfiguration);
+		this(cacheConfiguration, Ticker.systemTicker(), false);
 
 	}
 
 	/**
 	 * This constructor is used for testing purposes only.
 	 *
+	 * @param cacheConfiguration
+	 *            sets the cache configuration used to configure or disable the
+	 *            cache.
 	 * @param cacheTicker
 	 *            will be used in the cache to determine the time.
 	 * @param sameThreadCache
 	 *            set to true disables maintenance jobs of the cache. This makes the
 	 *            cache slower but more predictable for testing.
-	 * @param cacheConfiguration
-	 *            sets the cache configuration used to configure or disable the
-	 *            cache.
 	 */
-	AbstractOAuth2TokenService(Ticker cacheTicker, boolean sameThreadCache, CacheConfiguration cacheConfiguration) {
+	AbstractOAuth2TokenService(CacheConfiguration cacheConfiguration, Ticker cacheTicker, boolean sameThreadCache) {
 		Assertions.assertNotNull(cacheConfiguration, "cacheConfiguration is required");
 		this.cacheConfiguration = cacheConfiguration;
 		this.responseCache = createResponseCache(cacheTicker, sameThreadCache);
@@ -72,7 +75,8 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 	@Override
 	public OAuth2TokenResponse retrieveAccessTokenViaClientCredentialsGrant(@Nonnull URI tokenEndpointUri,
 			@Nonnull ClientCredentials clientCredentials,
-			@Nullable String subdomain, @Nullable Map<String, String> optionalParameters)
+			@Nullable String subdomain, @Nullable Map<String, String> optionalParameters,
+			boolean disableCacheForRequest)
 			throws OAuth2ServiceException {
 		assertNotNull(tokenEndpointUri, "tokenEndpointUri is required");
 		assertNotNull(clientCredentials, "clientCredentials is required");
@@ -85,7 +89,7 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 
 		HttpHeaders headers = HttpHeadersFactory.createWithoutAuthorizationHeader();
 
-		return getOAuth2TokenResponse(tokenEndpointUri, headers, parameters, subdomain);
+		return getOAuth2TokenResponse(tokenEndpointUri, headers, parameters, subdomain, disableCacheForRequest);
 	}
 
 	@Override
@@ -105,13 +109,14 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 
 		HttpHeaders headers = HttpHeadersFactory.createWithAuthorizationBearerHeader(token);
 
-		return getOAuth2TokenResponse(tokenEndpointUri, headers, parameters, subdomain);
+		return getOAuth2TokenResponse(tokenEndpointUri, headers, parameters, subdomain, false);
 	}
 
 	@Override
 	public OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(@Nonnull URI tokenEndpointUri,
 			@Nonnull ClientCredentials clientCredentials,
-			@Nonnull String refreshToken, String subdomain) throws OAuth2ServiceException {
+			@Nonnull String refreshToken, String subdomain, boolean disableCacheForRequest)
+			throws OAuth2ServiceException {
 		assertNotNull(tokenEndpointUri, "tokenEndpointUri is required");
 		assertNotNull(clientCredentials, "clientCredentials is required");
 		assertNotNull(refreshToken, "refreshToken is required");
@@ -124,13 +129,14 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 
 		HttpHeaders headers = HttpHeadersFactory.createWithoutAuthorizationHeader();
 
-		return getOAuth2TokenResponse(tokenEndpointUri, headers, parameters, subdomain);
+		return getOAuth2TokenResponse(tokenEndpointUri, headers, parameters, subdomain, disableCacheForRequest);
 	}
 
 	@Override
 	public OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(@Nonnull URI tokenEndpoint,
 			@Nonnull ClientCredentials clientCredentials, @Nonnull String username, @Nonnull String password,
-			@Nullable String subdomain, @Nullable Map<String, String> optionalParameters)
+			@Nullable String subdomain, @Nullable Map<String, String> optionalParameters,
+			boolean disableCacheForRequest)
 			throws OAuth2ServiceException {
 		assertNotNull(tokenEndpoint, "tokenEndpoint is required");
 		assertNotNull(clientCredentials, "clientCredentials are required");
@@ -147,13 +153,14 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 
 		HttpHeaders headers = HttpHeadersFactory.createWithoutAuthorizationHeader();
 
-		return getOAuth2TokenResponse(tokenEndpoint, headers, parameters, subdomain);
+		return getOAuth2TokenResponse(tokenEndpoint, headers, parameters, subdomain, disableCacheForRequest);
 	}
 
 	@Override
 	public OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(URI tokenEndpoint,
 			ClientCredentials clientCredentials, String token, @Nullable String subdomain,
-			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+			@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest)
+			throws OAuth2ServiceException {
 		assertNotNull(tokenEndpoint, "tokenEndpoint is required");
 		assertNotNull(clientCredentials, "clientCredentials are required");
 		assertNotNull(token, "token is required");
@@ -167,7 +174,7 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 
 		HttpHeaders headers = HttpHeadersFactory.createWithoutAuthorizationHeader();
 
-		return getOAuth2TokenResponse(tokenEndpoint, headers, parameters, subdomain);
+		return getOAuth2TokenResponse(tokenEndpoint, headers, parameters, subdomain, disableCacheForRequest);
 	}
 
 	/**
@@ -190,27 +197,50 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 
 	private OAuth2TokenResponse getOAuth2TokenResponse(@Nonnull URI tokenEndpointUri, HttpHeaders headers,
 			Map<String, String> additionalParameters,
-			@Nullable String subdomain) throws OAuth2ServiceException {
+			@Nullable String subdomain, boolean disableCacheForRequest) throws OAuth2ServiceException {
 		URI tokenEndpointUriWithSubdomainReplaced = UriUtil.replaceSubdomain(tokenEndpointUri, subdomain);
-		if (isCacheEnabled()) {
-			return getOrRequestAccessToken(tokenEndpointUriWithSubdomainReplaced, headers, additionalParameters);
+		if (isCacheDisabled() || disableCacheForRequest) {
+			return requestAccessToken(tokenEndpointUriWithSubdomainReplaced, headers, additionalParameters);
 		}
-		return requestAccessToken(tokenEndpointUriWithSubdomainReplaced, headers, additionalParameters);
+		return getOrRequestAccessToken(tokenEndpointUriWithSubdomainReplaced, headers, additionalParameters);
 	}
 
-	private OAuth2TokenResponse getOrRequestAccessToken(URI tokenEndpointUriWithSubdomainReplaced, HttpHeaders headers,
+	private OAuth2TokenResponse getOrRequestAccessToken(URI tokenEndpoint, HttpHeaders headers,
 			Map<String, String> parameters) throws OAuth2ServiceException {
-		CacheKey cacheKey = new CacheKey(tokenEndpointUriWithSubdomainReplaced, headers, parameters);
+		CacheKey cacheKey = new CacheKey(tokenEndpoint, headers, parameters);
 		OAuth2TokenResponse oAuth2TokenResponse = responseCache.getIfPresent(cacheKey);
 		if (oAuth2TokenResponse == null) {
-			responseCache.put(cacheKey, requestAccessToken(tokenEndpointUriWithSubdomainReplaced, headers,
-					parameters));
+			getAndCacheToken(cacheKey);
+		} else {
+			// check if token in cache should be refreshed
+			Duration delta = getCacheConfiguration().getTokenExpirationDelta();
+			Instant expiration = oAuth2TokenResponse.getExpiredAt().minus(delta);
+			if (expiration.isBefore(Instant.now(getClock()))) {
+				// refresh (soon) expired token
+				getAndCacheToken(cacheKey);
+			}
 		}
 		return responseCache.getIfPresent(cacheKey);
 	}
 
-	private boolean isCacheEnabled() {
-		return !CacheConfiguration.CACHE_DISABLED.equals(getCacheConfiguration());
+	/**
+	 * By default {@link Clock#systemUTC()} is used to determine of a cached token
+	 * has reached its expiration (exp) point in time. This method can be overridden
+	 * for testing purposes.
+	 *
+	 * @return the {@link Clock}
+	 */
+	protected Clock getClock() {
+		return Clock.systemUTC();
+	}
+
+	private void getAndCacheToken(CacheKey cacheKey) throws OAuth2ServiceException {
+		responseCache.put(cacheKey,
+				requestAccessToken(cacheKey.tokenEndpointUri, cacheKey.headers, cacheKey.parameters));
+	}
+
+	private boolean isCacheDisabled() {
+		return CacheConfiguration.CACHE_DISABLED.equals(getCacheConfiguration());
 	}
 
 	private Cache<CacheKey, OAuth2TokenResponse> createResponseCache(Ticker cacheTicker, boolean sameThreadCache) {
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
index 2b4a49c2..e6e97913 100644
--- 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
@@ -1,5 +1,6 @@
 package com.sap.cloud.security.xsuaa.client;
 
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.net.URI;
 import java.util.Map;
@@ -23,13 +24,34 @@ public interface OAuth2TokenService {
 	 *            supplying a subdomain (tenant).
 	 * @param optionalParameters
 	 *            optional request parameters, can be null.
+	 * @param disableCacheForRequest
+	 *            set to true disables the token cache for this request.
 	 * @return the OAuth2AccessToken.
 	 * @throws OAuth2ServiceException
 	 *             in case of an error during the http request.
 	 */
-	OAuth2TokenResponse retrieveAccessTokenViaClientCredentialsGrant(URI tokenEndpointUri,
+	OAuth2TokenResponse retrieveAccessTokenViaClientCredentialsGrant(@Nonnull URI tokenEndpointUri,
+			@Nonnull ClientCredentials clientCredentials,
+			@Nullable String subdomain, @Nullable Map<String, String> optionalParameters,
+			boolean disableCacheForRequest)
+			throws OAuth2ServiceException;
+
+	/**
+	 * Same as
+	 * {@link #retrieveAccessTokenViaClientCredentialsGrant(URI, ClientCredentials, String, Map, boolean)}
+	 * except that disableCacheForRequest is set to {@code false}.
+	 * 
+	 * @deprecated gets removed in favor of
+	 *             {@link #retrieveAccessTokenViaClientCredentialsGrant(URI, ClientCredentials, String, Map, boolean)}
+	 *             with next major version 3.0.0
+	 */
+	@Deprecated
+	default OAuth2TokenResponse retrieveAccessTokenViaClientCredentialsGrant(URI tokenEndpointUri,
 			ClientCredentials clientCredentials, @Nullable String subdomain,
-			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException;
+			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+		return retrieveAccessTokenViaClientCredentialsGrant(tokenEndpointUri, clientCredentials, subdomain,
+				optionalParameters, false);
+	}
 
 	/**
 	 * Exchanges user access token from OAuth Server with user access token. This
@@ -74,12 +96,31 @@ public interface OAuth2TokenService {
 	 * @param subdomain
 	 *            optionally indicates what Identity Zone this request goes to by
 	 *            supplying a subdomain (tenant).
+	 * @param disableCacheForRequest
+	 *            set to true disables the token cache for this request.
 	 * @return the OAuth2AccessToken
 	 * @throws OAuth2ServiceException
 	 *             in case of an error during the http request.
 	 */
 	OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(URI tokenEndpointUri, ClientCredentials clientCredentials,
-			String refreshToken, @Nullable String subdomain) throws OAuth2ServiceException;
+			String refreshToken, @Nullable String subdomain, boolean disableCacheForRequest)
+			throws OAuth2ServiceException;
+
+	/**
+	 * Same as
+	 * {@link #retrieveAccessTokenViaRefreshToken(URI, ClientCredentials, String, String, boolean)}
+	 * except that disableCacheForRequest is set to {@code false}.
+	 * 
+	 * @deprecated gets removed in favor of
+	 *             {@link #retrieveAccessTokenViaRefreshToken(URI, ClientCredentials, String, String, boolean)}
+	 *             with next major version 3.0.0
+	 */
+	@Deprecated
+	default OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(URI tokenEndpointUri,
+			ClientCredentials clientCredentials,
+			String refreshToken, @Nullable String subdomain) throws OAuth2ServiceException {
+		return retrieveAccessTokenViaRefreshToken(tokenEndpointUri, clientCredentials, refreshToken, subdomain, false);
+	}
 
 	/**
 	 * Requests access token from OAuth Server with user / password.
@@ -98,13 +139,34 @@ public interface OAuth2TokenService {
 	 *            supplying a subdomain (tenant).
 	 * @param optionalParameters
 	 *            optional request parameters, can be null.
+	 * @param disableCacheForRequest
+	 *            set to true disables the token cache for this request.
 	 * @return the OAuth2AccessToken
 	 * @throws OAuth2ServiceException
 	 *             in case of an error during the http request.
 	 */
 	OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(URI tokenEndpointUri, ClientCredentials clientCredentials,
 			String username, String password, @Nullable String subdomain,
-			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException;
+			@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest)
+			throws OAuth2ServiceException;
+
+	/**
+	 * Same as
+	 * {@link #retrieveAccessTokenViaPasswordGrant(URI, ClientCredentials, String, String, String, Map, boolean)}
+	 * except that disableCacheForRequest is set to {@code false}.
+	 * 
+	 * @deprecated gets removed in favor of
+	 *             {@link #retrieveAccessTokenViaPasswordGrant(URI, ClientCredentials, String, String, String, Map, boolean)}
+	 *             with next major version 3.0.0
+	 */
+	@Deprecated
+	default OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(URI tokenEndpointUri,
+			ClientCredentials clientCredentials,
+			String username, String password, @Nullable String subdomain,
+			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+		return retrieveAccessTokenViaPasswordGrant(tokenEndpointUri, clientCredentials, username, password, subdomain,
+				optionalParameters, false);
+	}
 
 	/**
 	 * @param tokenEndpointUri
@@ -120,12 +182,32 @@ public interface OAuth2TokenService {
 	 *            supplying a subdomain (tenant).
 	 * @param optionalParameters
 	 *            optional request parameters, can be null.
+	 * @param disableCacheForRequest
+	 *            set to true disables the token cache for this request.
 	 * @return the OAuth2AccessToken
 	 * @throws OAuth2ServiceException
 	 *             in case of an error during the http request.
 	 */
 	OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(URI tokenEndpointUri,
 			ClientCredentials clientCredentials, String token, @Nullable String subdomain,
-			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException;
+			@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest)
+			throws OAuth2ServiceException;
+
+	/**
+	 * Same as
+	 * {@link #retrieveAccessTokenViaJwtBearerTokenGrant(URI, ClientCredentials, String, String, Map, boolean)}
+	 * except that disableCacheForRequest is set to {@code false}.
+	 * 
+	 * @deprecated gets removed in favor of
+	 *             {@link #retrieveAccessTokenViaJwtBearerTokenGrant(URI, ClientCredentials, String, String, Map, boolean)}
+	 *             with next major version 3.0.0
+	 */
+	@Deprecated
+	default OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(URI tokenEndpointUri,
+			ClientCredentials clientCredentials, String token, @Nullable String subdomain,
+			@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+		return retrieveAccessTokenViaJwtBearerTokenGrant(tokenEndpointUri, clientCredentials, token, subdomain,
+				optionalParameters, false);
+	}
 
 }
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/CacheConfiguration.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/CacheConfiguration.java
index 973022a2..084803e7 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/CacheConfiguration.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/CacheConfiguration.java
@@ -7,43 +7,50 @@ import java.time.Duration;
 import java.util.Objects;
 
 /**
- * Data class to capture configuration options of caches.
+ * Data class to capture configuration options of token caches.
  */
 public class CacheConfiguration {
 
 	/**
 	 * Creates the default cache configuration.
 	 */
-	public static final CacheConfiguration DEFAULT = new CacheConfiguration(Duration.ofMinutes(10), 1000);
+	public static final CacheConfiguration DEFAULT = new CacheConfiguration(Duration.ofMinutes(10), 1000,
+			Duration.ofSeconds(30));
 
 	/**
 	 * Special cache configuration used to disable caching.
 	 */
-	public static final CacheConfiguration CACHE_DISABLED = new CacheConfiguration(Duration.ZERO, 0);
+	public static final CacheConfiguration CACHE_DISABLED = new CacheConfiguration(Duration.ZERO, 0, Duration.ZERO);
 
 	private final Duration expireAfterWrite;
 	private final int cacheSize;
+	private final Duration tokenExpirationDelta;
 
 	/**
 	 * Creates a new {@link CacheConfiguration} instance with the given properties.
-	 * See {@link CacheConfiguration#getExpireAfterWrite()} and
-	 * {@link CacheConfiguration#getCacheSize()} for a explanation of the respective
-	 * properties.
+	 * See {@link CacheConfiguration#getExpireAfterWrite()},
+	 * {@link CacheConfiguration#getCacheSize()} and
+	 * {@link CacheConfiguration#getTokenExpirationDelta()} for a explanation of the
+	 * respective properties.
 	 *
 	 * @param expireAfterWrite
 	 *            the expire after write cache property.
 	 * @param cacheSize
 	 *            the cache size property.
+	 * @param tokenExpirationDelta
+	 *            the token expiration delta.
 	 * @return a new {@link CacheConfiguration} instance.
 	 */
-	public static CacheConfiguration getInstance(@Nonnull Duration expireAfterWrite, int cacheSize) {
+	public static CacheConfiguration getInstance(@Nonnull Duration expireAfterWrite, int cacheSize,
+			@Nonnull Duration tokenExpirationDelta) {
 		Assertions.assertNotNull(expireAfterWrite, "Expire after write must not be null!");
-		return new CacheConfiguration(expireAfterWrite, cacheSize);
+		return new CacheConfiguration(expireAfterWrite, cacheSize, tokenExpirationDelta);
 	}
 
-	private CacheConfiguration(Duration expireAfterWrite, int cacheSize) {
+	private CacheConfiguration(Duration expireAfterWrite, int cacheSize, Duration tokenExpirationDelta) {
 		this.expireAfterWrite = expireAfterWrite;
 		this.cacheSize = cacheSize;
+		this.tokenExpirationDelta = tokenExpirationDelta;
 	}
 
 	/**
@@ -66,6 +73,22 @@ public class CacheConfiguration {
 		return cacheSize;
 	}
 
+	/**
+	 * If a cached token expires because its expiration time (exp) has been reached,
+	 * it should not be retrieved from the cache. A new token should be requested in
+	 * this case. For this to work cached tokens are being checked if their
+	 * expiration time (exp) has already been reached or if it is soon going to be
+	 * reached. The expiration delta controls what <em>soon</em> means. For example
+	 * if the expiration delta is set to 10 seconds and a cached token will expire
+	 * in 5 seconds, it will not be retrieved from cache anymore because the
+	 * duration until the token is expired is smaller than delta.
+	 *
+	 * @return the token expiration delta.
+	 */
+	public Duration getTokenExpirationDelta() {
+		return tokenExpirationDelta;
+	}
+
 	@Override
 	public boolean equals(Object o) {
 		if (this == o)
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlow.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlow.java
index 7ca52d05..ca21c165 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlow.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlow.java
@@ -26,6 +26,7 @@ public class ClientCredentialsTokenFlow {
 
 	private XsuaaTokenFlowRequest request;
 	private OAuth2TokenService tokenService;
+	private boolean disableCache = false;
 
 	/**
 	 * Creates a new instance.
@@ -76,6 +77,18 @@ public class ClientCredentialsTokenFlow {
 		return this;
 	}
 
+	/**
+	 * Can be used to disable the cache for the flow.
+	 * 
+	 * @param disableCache
+	 *            - disables cache when set to {@code true}.
+	 * @return this builder.
+	 */
+	public ClientCredentialsTokenFlow disableCache(boolean disableCache) {
+		this.disableCache = disableCache;
+		return this;
+	}
+
 	/**
 	 * Executes the token flow and returns a JWT token from XSUAA.
 	 *
@@ -132,7 +145,7 @@ public class ClientCredentialsTokenFlow {
 			OAuth2TokenResponse accessToken = tokenService
 					.retrieveAccessTokenViaClientCredentialsGrant(request.getTokenEndpoint(),
 							new ClientCredentials(request.getClientId(), request.getClientSecret()),
-							request.getSubdomain(), requestParameter);
+							request.getSubdomain(), requestParameter, disableCache);
 			return accessToken;
 		} catch (OAuth2ServiceException e) {
 			throw new TokenFlowException(
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlow.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlow.java
index ad4719da..9e23ac2f 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlow.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlow.java
@@ -14,6 +14,7 @@ public class PasswordTokenFlow {
 	private String password;
 	private String subdomain;
 	private Map<String, String> optionalParameters;
+	private boolean disableCache = false;
 
 	public PasswordTokenFlow(@Nonnull OAuth2TokenService tokenService,
 			@Nonnull OAuth2ServiceEndpointsProvider endpointsProvider,
@@ -44,7 +45,7 @@ public class PasswordTokenFlow {
 		try {
 			return tokenService
 					.retrieveAccessTokenViaPasswordGrant(endpointsProvider.getTokenEndpoint(), clientCredentials,
-							username, password, subdomain, optionalParameters);
+							username, password, subdomain, optionalParameters, disableCache);
 		} catch (OAuth2ServiceException e) {
 			throw new TokenFlowException(
 					String.format("Error requesting user token with grant_type 'client_credentials': %s",
@@ -103,6 +104,18 @@ public class PasswordTokenFlow {
 		return this;
 	}
 
+	/**
+	 * Can be used to disable the cache for the flow.
+	 *
+	 * @param disableCache
+	 *            - disables cache when set to {@code true}.
+	 * @return this builder.
+	 */
+	public PasswordTokenFlow disableCache(boolean disableCache) {
+		this.disableCache = disableCache;
+		return this;
+	}
+
 	private void checkParameter(String parameter, String message) {
 		if (parameter == null) {
 			throw new IllegalStateException(message);
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlow.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlow.java
index 4176f11a..f56ecb6f 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlow.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlow.java
@@ -19,6 +19,7 @@ public class RefreshTokenFlow {
 	private XsuaaTokenFlowRequest request;
 	private String refreshToken;
 	private OAuth2TokenService tokenService;
+	private boolean disableCache = false;
 
 	/**
 	 * Creates a new instance.
@@ -68,6 +69,18 @@ public class RefreshTokenFlow {
 		return this;
 	}
 
+	/**
+	 * Can be used to disable the cache for the flow.
+	 *
+	 * @param disableCache
+	 *            - disables cache when set to {@code true}.
+	 * @return this builder.
+	 */
+	public RefreshTokenFlow disableCache(boolean disableCache) {
+		this.disableCache = disableCache;
+		return this;
+	}
+
 	/**
 	 * Executes the refresh token flow against XSUAA.
 	 * 
@@ -130,7 +143,7 @@ public class RefreshTokenFlow {
 			OAuth2TokenResponse accessToken = tokenService.retrieveAccessTokenViaRefreshToken(
 					request.getTokenEndpoint(),
 					new ClientCredentials(request.getClientId(), request.getClientSecret()), refreshToken,
-					request.getSubdomain());
+					request.getSubdomain(), disableCache);
 			return accessToken;
 		} catch (OAuth2ServiceException e) {
 			throw new TokenFlowException(
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlow.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlow.java
index 5a8ebf02..d398a609 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlow.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlow.java
@@ -32,6 +32,7 @@ public class UserTokenFlow {
 	private XsuaaTokenFlowRequest request;
 	private String token;
 	private OAuth2TokenService tokenService;
+	private boolean disableCache = false;
 
 	/**
 	 * Creates a new instance.
@@ -95,6 +96,18 @@ public class UserTokenFlow {
 		return this;
 	}
 
+	/**
+	 * Can be used to disable the cache for the flow.
+	 *
+	 * @param disableCache
+	 *            - disables cache when set to {@code true}.
+	 * @return this builder.
+	 */
+	public UserTokenFlow disableCache(boolean disableCache) {
+		this.disableCache = disableCache;
+		return this;
+	}
+
 	/**
 	 * Executes this flow against the XSUAA endpoint. As a result the exchanged JWT
 	 * token is returned. <br>
@@ -155,14 +168,11 @@ public class UserTokenFlow {
 			optionalParameter.put(AUTHORITIES, authorities); // places JSON inside the URI !?!
 		}
 
-		String refreshToken = null;
 		try {
 			return tokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
 					request.getTokenEndpoint(),
 					new ClientCredentials(request.getClientId(), request.getClientSecret()),
-					token,
-					request.getSubdomain(),
-					optionalParameter);
+					token, request.getSubdomain(), optionalParameter, disableCache);
 		} catch (OAuth2ServiceException e) {
 			throw new TokenFlowException(
 					String.format(


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterIntegrationTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterIntegrationTest.java
index 55ecfd57..a71c8812 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterIntegrationTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterIntegrationTest.java
@@ -377,7 +377,8 @@ public class XSUserInfoAdapterIntegrationTest {
 				eq(clientCredentials),
 				eq(correctEnduserInfo.getAppToken()),
 				any(),
-				anyMap())).thenReturn(oAuth2TokenResponse);
+				anyMap(),
+				anyBoolean())).thenReturn(oAuth2TokenResponse);
 		when(oAuth2TokenResponse.getAccessToken()).thenReturn(testToken);
 
 		// execute flow
diff --git a/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerTestConfiguration.java b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerTestConfiguration.java
index 8626b312..8d5fa8bd 100644
--- a/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerTestConfiguration.java
+++ b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerTestConfiguration.java
@@ -38,7 +38,8 @@ public class TokenBrokerTestConfiguration {
 			@Override
 			public OAuth2TokenResponse retrieveAccessTokenViaClientCredentialsGrant(URI tokenEndpointUri,
 					ClientCredentials clientCredentials, @Nullable String subdomain,
-					@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+					@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest)
+					throws OAuth2ServiceException {
 				try {
 					return new OAuth2TokenResponse("token_cc", 100, null);
 				} catch (Exception e) {
@@ -49,13 +50,15 @@ public class TokenBrokerTestConfiguration {
 			@Override
 			public OAuth2TokenResponse retrieveAccessTokenViaUserTokenGrant(URI tokenEndpointUri,
 					ClientCredentials clientCredentials, String token, @Nullable String subdomain,
-					@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+					@Nullable Map<String, String> optionalParameters)
+					throws OAuth2ServiceException {
 				return null;
 			}
 
 			@Override
 			public OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(URI tokenEndpointUri,
-					ClientCredentials clientCredentials, String refreshToken, @Nullable String subdomain)
+					ClientCredentials clientCredentials, String refreshToken, @Nullable String subdomain,
+					boolean disableCacheForRequest)
 					throws OAuth2ServiceException {
 				return null;
 			}
@@ -63,7 +66,8 @@ public class TokenBrokerTestConfiguration {
 			@Override
 			public OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(URI tokenEndpointUri,
 					ClientCredentials clientCredentials, String username, String password, @Nullable String subdomain,
-					@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+					@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest)
+					throws OAuth2ServiceException {
 				try {
 					if ("https://mydomain.auth.com/oauth/token".equals(tokenEndpointUri.toString())) {
 						if ("myuser".equals(username) && "mypass".equals(password)
@@ -86,7 +90,8 @@ public class TokenBrokerTestConfiguration {
 			@Override
 			public OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(URI tokenEndpointUri,
 					ClientCredentials clientCredentials, String token, @Nullable String subdomain,
-					@Nullable Map<String, String> optionalParameters) throws OAuth2ServiceException {
+					@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest)
+					throws OAuth2ServiceException {
 				return null;
 			}
 		};
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenServiceTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenServiceTest.java
index a9715d84..74ce2933 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenServiceTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/client/AbstractOAuth2TokenServiceTest.java
@@ -8,22 +8,26 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.net.URI;
-import java.time.Duration;
+import java.time.*;
 import java.util.Map;
 
+import static java.time.ZoneOffset.UTC;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class AbstractOAuth2TokenServiceTest {
 
 	public static final URI TOKEN_ENDPOINT_URI = URI.create("http://test.token.endpoint/oauth/token");
 	public static final String SUBDOMAIN = "subdomain";
-	private final static TestCacheTicker TEST_CACHE_TICKER = new TestCacheTicker();
+	private static final Instant NOW = LocalDateTime.of(2020, 1, 1, 0, 0, 0, 0).toInstant(UTC);
+	public static final CacheConfiguration TEST_CACHE_CONFIGURATION = CacheConfiguration.DEFAULT;
+
 	private TestOAuth2TokenService cut;
 
 	@Before
 	public void setUp() {
-		cut = new TestOAuth2TokenService(CacheConfiguration.DEFAULT);
-		TEST_CACHE_TICKER.reset();
+		cut = new TestOAuth2TokenService(TEST_CACHE_CONFIGURATION);
 	}
 
 	@Test
@@ -63,7 +67,7 @@ public class AbstractOAuth2TokenServiceTest {
 	public void retrieveAccessTokenViaClientCredentials_twoDistinctRequests_onlyTwoRequestCalls()
 			throws OAuth2ServiceException {
 		retrieveAccessTokenViaClientCredentials();
-		retrieveAccessTokenViaClientCredentials(new ClientCredentials("other client id", "secret"));
+		retrieveAccessTokenViaClientCredentials(new ClientCredentials("other client id", "secret"), false);
 		retrieveAccessTokenViaClientCredentials();
 
 		assertThat(cut.tokenRequestCallCount).isEqualTo(2);
@@ -148,7 +152,7 @@ public class AbstractOAuth2TokenServiceTest {
 	@Test
 	public void requestAccessToken_tokensAreInvalidatedAfterTime_requestsFreshToken() throws OAuth2ServiceException {
 		OAuth2TokenResponse firstResponse = retrieveAccessTokenViaClientCredentials();
-		TEST_CACHE_TICKER.advance(CacheConfiguration.DEFAULT.getExpireAfterWrite());
+		cut.advanceTime(TEST_CACHE_CONFIGURATION.getExpireAfterWrite());
 		OAuth2TokenResponse secondResponse = retrieveAccessTokenViaClientCredentials();
 
 		assertThat(cut.tokenRequestCallCount).isEqualTo(2);
@@ -157,7 +161,7 @@ public class AbstractOAuth2TokenServiceTest {
 
 	@Test
 	public void requestAccessToken_cacheIsFull_requestsFreshToken() throws OAuth2ServiceException {
-		cut = new TestOAuth2TokenService(CacheConfiguration.getInstance(Duration.ofMinutes(10), 1));
+		cut = new TestOAuth2TokenService(cacheConfigurationWithSize(1));
 		OAuth2TokenResponse user1Response = retrieveAccessTokenViaPasswordGrant("user1");
 		OAuth2TokenResponse user2Response = retrieveAccessTokenViaPasswordGrant("user2");
 		OAuth2TokenResponse secondUser1Response = retrieveAccessTokenViaPasswordGrant("user1");
@@ -166,6 +170,51 @@ public class AbstractOAuth2TokenServiceTest {
 		assertThat(cut.tokenRequestCallCount).isEqualTo(3);
 	}
 
+	@Test
+	public void requestAccessToken_cacheDisabledForRequest_requestsFreshTokens() throws OAuth2ServiceException {
+		OAuth2TokenResponse firstResponse = retrieveAccessTokenViaClientCredentials(clientCredentials(), false);
+		OAuth2TokenResponse secondResponse = retrieveAccessTokenViaClientCredentials(clientCredentials(), true);
+		OAuth2TokenResponse lastResponse = retrieveAccessTokenViaClientCredentials(clientCredentials(), false);
+
+		assertThat(cut.tokenRequestCallCount).isEqualTo(2);
+		assertThat(firstResponse).isNotSameAs(secondResponse);
+		assertThat(firstResponse).isSameAs(lastResponse);
+	}
+
+	@Test
+	public void requestAccessToken_expiredToken_requestsFreshTokens() throws OAuth2ServiceException {
+		cut = new TestOAuth2TokenService(TEST_CACHE_CONFIGURATION);
+		cut.setExpiredAt(NOW.minus(Duration.ofHours(1)));
+
+		retrieveAccessTokenViaClientCredentials();
+		retrieveAccessTokenViaClientCredentials();
+
+		assertThat(cut.tokenRequestCallCount).isEqualTo(2);
+	}
+
+	@Test
+	public void requestAccessToken_expireNotInDelta_cacheUsed() throws OAuth2ServiceException {
+		cut = new TestOAuth2TokenService(cacheConfigurationWithDelta(Duration.ofSeconds(10)));
+		cut.setExpiredAt(NOW.plus(Duration.ofSeconds(30)));
+
+		retrieveAccessTokenViaClientCredentials();
+		retrieveAccessTokenViaClientCredentials();
+
+		assertThat(cut.tokenRequestCallCount).isEqualTo(1);
+	}
+
+	@Test
+	public void requestAccessToken_timeAdvancedAndGotExpired_requestsFreshToken() throws OAuth2ServiceException {
+		cut = new TestOAuth2TokenService(cacheConfigurationWithDelta(Duration.ofSeconds(10)));
+		cut.setExpiredAt(NOW.plus(Duration.ofSeconds(30)));
+
+		retrieveAccessTokenViaClientCredentials();
+		cut.advanceTime(Duration.ofSeconds(25));
+		retrieveAccessTokenViaClientCredentials();
+
+		assertThat(cut.tokenRequestCallCount).isEqualTo(2);
+	}
+
 	private OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(String token) throws OAuth2ServiceException {
 		return retrieveAccessTokenViaJwtBearerTokenGrant(token, null);
 	}
@@ -177,24 +226,25 @@ public class AbstractOAuth2TokenServiceTest {
 	}
 
 	private OAuth2TokenResponse retrieveAccessTokenViaClientCredentials() throws OAuth2ServiceException {
-		return retrieveAccessTokenViaClientCredentials(clientCredentials());
+		return retrieveAccessTokenViaClientCredentials(clientCredentials(), false);
 	}
 
-	private OAuth2TokenResponse retrieveAccessTokenViaClientCredentials(ClientCredentials clientCredentials)
+	private OAuth2TokenResponse retrieveAccessTokenViaClientCredentials(ClientCredentials clientCredentials,
+			boolean disableCacheForRequest)
 			throws OAuth2ServiceException {
 		return cut.retrieveAccessTokenViaClientCredentialsGrant(TOKEN_ENDPOINT_URI, clientCredentials, SUBDOMAIN,
-				null);
+				null, disableCacheForRequest);
 	}
 
 	private OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(String username) throws OAuth2ServiceException {
 		return cut.retrieveAccessTokenViaPasswordGrant(TOKEN_ENDPOINT_URI, clientCredentials(), username, "password",
-				SUBDOMAIN, null);
+				SUBDOMAIN, null, false);
 	}
 
 	private OAuth2TokenResponse retrieveAccessTokenViaPasswordGrant(URI tokenEndpointUri)
 			throws OAuth2ServiceException {
 		return cut.retrieveAccessTokenViaPasswordGrant(tokenEndpointUri, clientCredentials(), "username", "password",
-				SUBDOMAIN, null);
+				SUBDOMAIN, null, false);
 	}
 
 	private OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(String refreshToken) throws OAuth2ServiceException {
@@ -203,28 +253,59 @@ public class AbstractOAuth2TokenServiceTest {
 
 	private OAuth2TokenResponse retrieveAccessTokenViaRefreshToken(String refreshToken, String subdomain)
 			throws OAuth2ServiceException {
-		return cut.retrieveAccessTokenViaRefreshToken(TOKEN_ENDPOINT_URI, clientCredentials(), refreshToken, subdomain);
+		return cut.retrieveAccessTokenViaRefreshToken(TOKEN_ENDPOINT_URI, clientCredentials(), refreshToken, subdomain,
+				false);
 	}
 
 	private ClientCredentials clientCredentials() {
 		return new ClientCredentials("clientId", "clientSecret");
 	}
 
+	private CacheConfiguration cacheConfigurationWithDelta(Duration delta) {
+		return CacheConfiguration.getInstance(TEST_CACHE_CONFIGURATION.getExpireAfterWrite(),
+				TEST_CACHE_CONFIGURATION.getCacheSize(), delta);
+	}
+
+	private CacheConfiguration cacheConfigurationWithSize(int size) {
+		return CacheConfiguration.getInstance(TEST_CACHE_CONFIGURATION.getExpireAfterWrite(), size,
+				TEST_CACHE_CONFIGURATION.getTokenExpirationDelta());
+	}
+
 	private static class TestOAuth2TokenService extends AbstractOAuth2TokenService {
 
+		private final static TestCacheTicker testCacheTicker = new TestCacheTicker();
 		private int tokenRequestCallCount = 0;
+		private Instant expiredAt = NOW.plus(Duration.ofDays(1));
+		private Clock clock = Clock.fixed(NOW, UTC);
 
 		public TestOAuth2TokenService(CacheConfiguration cacheConfiguration) {
-			super(TEST_CACHE_TICKER, true, cacheConfiguration);
+			super(cacheConfiguration, testCacheTicker, true);
+			testCacheTicker.reset();
+		}
+
+		public void setExpiredAt(Instant expiredAt) {
+			this.expiredAt = expiredAt;
+		}
+
+		public void advanceTime(Duration duration) {
+			clock = Clock.offset(clock, duration);
+			testCacheTicker.advance(duration);
+		}
+
+		@Override
+		protected Clock getClock() {
+			return clock;
 		}
 
 		@Override
 		protected OAuth2TokenResponse requestAccessToken(URI tokenEndpointUri, HttpHeaders headers,
 				Map<String, String> parameters) {
 			tokenRequestCallCount++;
-			return new OAuth2TokenResponse("", 1L, null);
+			OAuth2TokenResponse responseMock = mock(OAuth2TokenResponse.class);
+			when(responseMock.getAccessToken()).thenReturn("token");
+			when(responseMock.getExpiredAt()).thenReturn(expiredAt);
+			return responseMock;
 		}
-
 	}
 
 	private static class TestCacheTicker implements Ticker {
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java
index 0613ed4c..a568c787 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersTest.java
@@ -38,6 +38,4 @@ public class HttpHeadersTest {
 		assertThat(headers2).isNotEqualTo(headers3);
 	}
 
-
-
 }
\ No newline at end of file
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlowTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlowTest.java
index 4cbe802b..ed40e68f 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlowTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/ClientCredentialsTokenFlowTest.java
@@ -4,9 +4,9 @@ import static com.sap.cloud.security.xsuaa.tokenflows.TestConstants.*;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNotNull;
-import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.times;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -15,7 +15,6 @@ 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 com.sap.cloud.security.xsuaa.client.ClientCredentials;
@@ -60,25 +59,21 @@ public class ClientCredentialsTokenFlowTest {
 	}
 
 	@Test
-	public void execute() throws TokenFlowException, OAuth2ServiceException {
-		OAuth2TokenResponse accessToken = new OAuth2TokenResponse(JWT_ACCESS_TOKEN, 441231, null);
-
-		Mockito.when(mockTokenService
-				.retrieveAccessTokenViaClientCredentialsGrant(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
-						isNull(), isNull()))
-				.thenReturn(accessToken);
+	public void execute_triggersServiceCallWithDefaults() throws TokenFlowException, OAuth2ServiceException {
+		OAuth2TokenResponse accessToken = mockRetrieveAccessToken();
 
-		OAuth2TokenResponse jwt = cut.execute();
+		OAuth2TokenResponse response = cut.execute();
 
-		assertThat(jwt.getAccessToken(), is(accessToken.getAccessToken()));
+		assertThat(response.getAccessToken(), is(accessToken.getAccessToken()));
+		verifyThatDisableCacheIs(false);
 	}
 
 	@Test
 	public void execute_throwsIfServiceRaisesException() throws OAuth2ServiceException {
-		Mockito.when(mockTokenService
+		when(mockTokenService
 				.retrieveAccessTokenViaClientCredentialsGrant(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
-						isNull(), isNull()))
-				.thenThrow(new OAuth2ServiceException("exception executed REST call"));
+						isNull(), isNull(), anyBoolean()))
+								.thenThrow(new OAuth2ServiceException("exception executed REST call"));
 
 		assertThatThrownBy(() -> {
 			cut.execute();
@@ -89,20 +84,46 @@ public class ClientCredentialsTokenFlowTest {
 
 	@Test
 	public void execute_withAdditionalAuthorities() throws TokenFlowException, OAuth2ServiceException {
-		OAuth2TokenResponse accessToken = new OAuth2TokenResponse(JWT_ACCESS_TOKEN, 441231, null);
-
-		Map<String, String> additionalAuthorities = new HashMap<String, String>();
+		OAuth2TokenResponse accessToken = mockRetrieveAccessToken();
+		Map<String, String> additionalAuthorities = new HashMap<>();
 		additionalAuthorities.put("DummyAttribute", "DummyAttributeValue");
 
-		Mockito.when(mockTokenService
+		OAuth2TokenResponse response = cut.attributes(additionalAuthorities).execute();
+
+		assertThat(response.getAccessToken(), is(accessToken.getAccessToken()));
+		verify(mockTokenService, times(1))
 				.retrieveAccessTokenViaClientCredentialsGrant(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
-						isNull(), isNotNull()))
-				.thenReturn(accessToken);
+						isNull(), isNotNull(), anyBoolean());
+	}
+
+	@Test
+	public void execute_withCacheDisabled() throws TokenFlowException, OAuth2ServiceException {
+		OAuth2TokenResponse accessToken = mockRetrieveAccessToken();
+
+		OAuth2TokenResponse response = cut.disableCache(true).execute();
 
-		OAuth2TokenResponse jwt = cut.attributes(additionalAuthorities)
-				.execute();
+		assertThat(response.getAccessToken(), is(accessToken.getAccessToken()));
+		verifyThatDisableCacheIs(true);
 
-		assertThat(jwt.getAccessToken(), is(accessToken.getAccessToken()));
+		cut.disableCache(false).execute();
+
+		verifyThatDisableCacheIs(false);
+
+	}
+
+	private void verifyThatDisableCacheIs(boolean disableCache) throws OAuth2ServiceException {
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaClientCredentialsGrant(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
+						isNull(), isNull(), eq(disableCache));
+	}
+
+	private OAuth2TokenResponse mockRetrieveAccessToken() throws OAuth2ServiceException {
+		OAuth2TokenResponse accessToken = new OAuth2TokenResponse(JWT_ACCESS_TOKEN, 441231, null);
+		when(mockTokenService
+				.retrieveAccessTokenViaClientCredentialsGrant(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
+						any(), any(), anyBoolean()))
+								.thenReturn(accessToken);
+		return accessToken;
 	}
 
 }
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlowTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlowTest.java
index 62248784..c51b95e4 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlowTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/PasswordTokenFlowTest.java
@@ -5,7 +5,6 @@ import org.assertj.core.util.Maps;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.Map;
@@ -13,6 +12,7 @@ import java.util.Map;
 import static com.sap.cloud.security.xsuaa.tokenflows.TestConstants.*;
 import static org.assertj.core.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
@@ -24,9 +24,9 @@ public class PasswordTokenFlowTest {
 	private PasswordTokenFlow cut;
 
 	@Before
-	public void setUp() throws OAuth2ServiceException {
-		tokenService = Mockito.mock(OAuth2TokenService.class);
-		endpointsProvider = Mockito.mock(OAuth2ServiceEndpointsProvider.class);
+	public void setUp() {
+		tokenService = mock(OAuth2TokenService.class);
+		endpointsProvider = mock(OAuth2ServiceEndpointsProvider.class);
 
 		when(endpointsProvider.getTokenEndpoint()).thenReturn(TOKEN_ENDPOINT_URI);
 
@@ -74,7 +74,7 @@ public class PasswordTokenFlowTest {
 
 	@Test
 	public void execute_returnsCorrectAccessTokenInResponse() throws Exception {
-		returnValidResponse();
+		mockValidResponse();
 
 		OAuth2TokenResponse actualResponse = executeRequest();
 
@@ -84,7 +84,7 @@ public class PasswordTokenFlowTest {
 	@Test
 	public void execute_ReturnsDifferentAccessTokenInResponse() throws Exception {
 		String otherAccessToken = "qwertyqwerty";
-		returnValidResponse(otherAccessToken);
+		mockValidResponse(otherAccessToken);
 
 		OAuth2TokenResponse actualResponse = executeRequest();
 
@@ -93,7 +93,7 @@ public class PasswordTokenFlowTest {
 
 	@Test
 	public void execute_ReturnsRefreshTokenInResponse() throws Exception {
-		returnValidResponse();
+		mockValidResponse();
 
 		OAuth2TokenResponse actualResponse = executeRequest();
 
@@ -104,10 +104,10 @@ public class PasswordTokenFlowTest {
 	public void allRequiredParametersAreUsed() throws Exception {
 		executeRequest();
 
-		Mockito.verify(tokenService, times(1))
+		verify(tokenService, times(1))
 				.retrieveAccessTokenViaPasswordGrant(eq(TOKEN_ENDPOINT_URI), eq(CLIENT_CREDENTIALS), eq(
 						USERNAME),
-						eq(PASSWORD), any(), any());
+						eq(PASSWORD), any(), any(), eq(false));
 	}
 
 	@Test
@@ -115,9 +115,18 @@ public class PasswordTokenFlowTest {
 		String newSubdomain = "staging";
 		createValidRequest().subdomain(newSubdomain).execute();
 
-		Mockito.verify(tokenService, times(1))
+		verify(tokenService, times(1))
 				.retrieveAccessTokenViaPasswordGrant(any(), any(), any(),
-						any(), eq(newSubdomain), any());
+						any(), eq(newSubdomain), any(), anyBoolean());
+	}
+
+	@Test
+	public void disableCacheIsUsed() throws Exception {
+		createValidRequest().disableCache(true).execute();
+		verifyThatDisableCacheIs(true);
+
+		createValidRequest().disableCache(false).execute();
+		verifyThatDisableCacheIs(false);
 	}
 
 	@Test
@@ -129,9 +138,9 @@ public class PasswordTokenFlowTest {
 
 		createValidRequest().optionalParameters(givenParameters).execute();
 
-		Mockito.verify(tokenService, times(1))
+		verify(tokenService, times(1))
 				.retrieveAccessTokenViaPasswordGrant(any(), any(), any(),
-						any(), any(), eq(equalParameters));
+						any(), any(), eq(equalParameters), anyBoolean());
 	}
 
 	private OAuth2TokenResponse executeRequest() throws TokenFlowException {
@@ -142,19 +151,24 @@ public class PasswordTokenFlowTest {
 		return cut.username(USERNAME).password(PASSWORD);
 	}
 
-	private void returnValidResponse() throws OAuth2ServiceException {
+	private void mockValidResponse() throws OAuth2ServiceException {
 		OAuth2TokenResponse validResponse = new OAuth2TokenResponse(ACCESS_TOKEN, EXPIRED_IN, REFRESH_TOKEN);
 		when(tokenService.retrieveAccessTokenViaPasswordGrant(TOKEN_ENDPOINT_URI, CLIENT_CREDENTIALS, USERNAME,
 				PASSWORD,
-				null, null))
+				null, null, false))
 						.thenReturn(validResponse);
 	}
 
-	private void returnValidResponse(String accessToken) throws OAuth2ServiceException {
+	private void mockValidResponse(String accessToken) throws OAuth2ServiceException {
 		OAuth2TokenResponse validResponse = new OAuth2TokenResponse(accessToken, EXPIRED_IN, REFRESH_TOKEN);
 		when(tokenService.retrieveAccessTokenViaPasswordGrant(TOKEN_ENDPOINT_URI, CLIENT_CREDENTIALS, USERNAME,
-				PASSWORD,
-				null, null))
+				PASSWORD, null, null, false))
 						.thenReturn(validResponse);
 	}
+
+	private void verifyThatDisableCacheIs(boolean disableCache) throws OAuth2ServiceException {
+		verify(tokenService, times(1))
+				.retrieveAccessTokenViaPasswordGrant(any(), any(), any(),
+						any(), any(), any(), eq(disableCache));
+	}
 }
\ No newline at end of file
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlowTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlowTest.java
index 515db625..72c2b68b 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlowTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/RefreshTokenFlowTest.java
@@ -4,14 +4,14 @@ import static com.sap.cloud.security.xsuaa.tokenflows.TestConstants.*;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.times;
 
 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 com.sap.cloud.security.xsuaa.client.ClientCredentials;
@@ -66,25 +66,30 @@ public class RefreshTokenFlowTest {
 
 	@Test
 	public void execute() throws TokenFlowException, OAuth2ServiceException {
-		OAuth2TokenResponse accessToken = new OAuth2TokenResponse(JWT_ACCESS_TOKEN, 441231, null);
+		OAuth2TokenResponse accessToken = mockRetrieveAccessToken();
 
-		Mockito.when(mockTokenService
-				.retrieveAccessTokenViaRefreshToken(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
-						eq(REFRESH_TOKEN), isNull()))
-				.thenReturn(accessToken);
+		OAuth2TokenResponse response = cut.refreshToken(REFRESH_TOKEN).execute();
 
-		OAuth2TokenResponse jwt = cut.refreshToken(REFRESH_TOKEN)
-				.execute();
+		assertThat(response.getAccessToken(), is(accessToken.getAccessToken()));
+		verifyRetrieveAccessTokenCalledWith(REFRESH_TOKEN, false);
+	}
 
-		assertThat(jwt.getAccessToken(), is(accessToken.getAccessToken()));
+	@Test
+	public void execute_disableCacheIsUsed() throws TokenFlowException, OAuth2ServiceException {
+		OAuth2TokenResponse accessToken = mockRetrieveAccessToken();
+
+		OAuth2TokenResponse response = cut.refreshToken(REFRESH_TOKEN).disableCache(true).execute();
+
+		assertThat(response.getAccessToken(), is(accessToken.getAccessToken()));
+		verifyRetrieveAccessTokenCalledWith(REFRESH_TOKEN, true);
 	}
 
 	@Test
 	public void execute_throwsIfServiceRaisesException() throws OAuth2ServiceException {
-		Mockito.when(mockTokenService
+		when(mockTokenService
 				.retrieveAccessTokenViaRefreshToken(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
-						eq(REFRESH_TOKEN), isNull()))
-				.thenThrow(new OAuth2ServiceException("exception executed REST call"));
+						eq(REFRESH_TOKEN), isNull(), anyBoolean()))
+								.thenThrow(new OAuth2ServiceException("exception executed REST call"));
 
 		assertThatThrownBy(() -> {
 			cut.refreshToken(REFRESH_TOKEN)
@@ -94,4 +99,20 @@ public class RefreshTokenFlowTest {
 						"Error refreshing token with grant_type 'refresh_token': exception executed REST call");
 	}
 
+	private void verifyRetrieveAccessTokenCalledWith(String refreshToken, boolean disableCacheForRequest)
+			throws OAuth2ServiceException {
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaRefreshToken(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
+						eq(refreshToken), isNull(), eq(disableCacheForRequest));
+	}
+
+	private OAuth2TokenResponse mockRetrieveAccessToken() throws OAuth2ServiceException {
+		OAuth2TokenResponse accessToken = new OAuth2TokenResponse(JWT_ACCESS_TOKEN, 441231, null);
+		when(mockTokenService
+				.retrieveAccessTokenViaRefreshToken(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
+						eq(REFRESH_TOKEN), isNull(), anyBoolean()))
+								.thenReturn(accessToken);
+		return accessToken;
+	}
+
 }
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlowTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlowTest.java
index c2b8aa41..b3085c02 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlowTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlowTest.java
@@ -5,6 +5,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
 import java.util.HashMap;
@@ -13,7 +15,6 @@ import java.util.Map;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import com.sap.cloud.security.xsuaa.client.ClientCredentials;
@@ -22,29 +23,21 @@ import com.sap.cloud.security.xsuaa.client.OAuth2ServiceEndpointsProvider;
 import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;
 import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;
 import com.sap.cloud.security.xsuaa.client.XsuaaDefaultEndpoints;
-import com.sap.cloud.security.xsuaa.test.JwtGenerator;
 
 @RunWith(MockitoJUnitRunner.class)
 public class UserTokenFlowTest {
 
-	@Mock
 	private OAuth2TokenService mockTokenService;
 
-	private String userTokenToBeExchanged;
-	private OAuth2TokenResponse dummyAccessToken;
-	private ClientCredentials clientCredentials;
-	private UserTokenFlow cut;
-	private OAuth2ServiceEndpointsProvider endpointsProvider;
+	private final String exchangeToken = "exchange token";
+	private final ClientCredentials clientCredentials = new ClientCredentials("clientId", "clientSecret");
+	private final OAuth2ServiceEndpointsProvider endpointsProvider = new XsuaaDefaultEndpoints(XSUAA_BASE_URI);
 
-	private static final String JWT_ACCESS_TOKEN = "4bfad399ca10490da95c2b5eb4451d53";
+	private UserTokenFlow cut;
 
 	@Before
 	public void setup() {
-		this.userTokenToBeExchanged = new JwtGenerator().getToken().getTokenValue();
-		;
-		this.dummyAccessToken = new OAuth2TokenResponse(JWT_ACCESS_TOKEN, 441231, REFRESH_TOKEN);
-		this.clientCredentials = new ClientCredentials("clientId", "clientSecret");
-		this.endpointsProvider = new XsuaaDefaultEndpoints(XSUAA_BASE_URI);
+		this.mockTokenService = mock(OAuth2TokenService.class);
 		this.cut = new UserTokenFlow(mockTokenService, endpointsProvider, clientCredentials);
 	}
 
@@ -65,80 +58,96 @@ public class UserTokenFlowTest {
 
 	@Test
 	public void execute_throwsIfMandatoryFieldsNotSet() {
-		assertThatThrownBy(() -> {
-			cut.execute();
-		}).isInstanceOf(IllegalStateException.class);
+		assertThatThrownBy(cut::execute)
+				.isInstanceOf(IllegalStateException.class);
 
-		assertThatThrownBy(() -> {
-			cut.execute();
-		}).isInstanceOf(IllegalStateException.class).hasMessageContaining("User token not set");
+		assertThatThrownBy(cut::execute)
+				.isInstanceOf(IllegalStateException.class)
+				.hasMessageContaining("User token not set");
 	}
 
 	@Test
 	public void execute_throwsIfServiceRaisesException() throws OAuth2ServiceException {
 		when(mockTokenService
-				.retrieveAccessTokenViaJwtBearerTokenGrant(eq(TOKEN_ENDPOINT_URI),
-						eq(clientCredentials),
-						eq(userTokenToBeExchanged),
-						isNull(), isNull()))
-								.thenThrow(new OAuth2ServiceException("exception executed REST call"));
+				.retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), any(), isNull(), isNull(), anyBoolean()))
+						.thenThrow(new OAuth2ServiceException("exception executed REST call"));
 
-		assertThatThrownBy(() -> {
-			cut.token(userTokenToBeExchanged)
-					.execute();
-		}).isInstanceOf(TokenFlowException.class)
+		assertThatThrownBy(() -> cut.token(exchangeToken).execute())
+				.isInstanceOf(TokenFlowException.class)
 				.hasMessageContaining(
 						"Error requesting token with grant_type 'urn:ietf:params:oauth:grant-type:jwt-bearer'");
 	}
 
 	@Test
-	public void execute() throws TokenFlowException, OAuth2ServiceException {
-		when(mockTokenService
-				.retrieveAccessTokenViaJwtBearerTokenGrant(eq(TOKEN_ENDPOINT_URI),
-						eq(clientCredentials),
-						eq(userTokenToBeExchanged),
-						isNull(), isNull()))
-								.thenReturn(dummyAccessToken);
+	public void execute_callsServiceWithDefaults() throws TokenFlowException, OAuth2ServiceException {
+		OAuth2TokenResponse mockedResponse = mockRetrieveAccessToken();
 
-		OAuth2TokenResponse jwt = cut.token(userTokenToBeExchanged)
-				.execute();
+		OAuth2TokenResponse response = cut.token(exchangeToken).execute();
 
-		assertThat(jwt.getAccessToken(), is(dummyAccessToken.getAccessToken()));
+		assertThat(response.getAccessToken(), is(mockedResponse.getAccessToken()));
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaJwtBearerTokenGrant(endpointsProvider.getTokenEndpoint(),
+						clientCredentials, exchangeToken, null,
+						null, false);
 	}
 
 	@Test
 	public void execute_withSubdomain() throws TokenFlowException, OAuth2ServiceException {
+		OAuth2TokenResponse mockedResponse = mockRetrieveAccessToken();
 		String subdomain = "subdomain";
-		cut = new UserTokenFlow(mockTokenService, endpointsProvider, clientCredentials);
 
-		when(mockTokenService
-				.retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), any(), eq(subdomain), any()))
-						.thenReturn(dummyAccessToken);
+		OAuth2TokenResponse response = cut.subdomain(subdomain).token(exchangeToken).execute();
+
+		assertThat(response.getAccessToken(), is(mockedResponse.getAccessToken()));
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), any(), eq(subdomain), any(), anyBoolean());
+	}
+
+	@Test
+	public void execute_withDisableCache() throws TokenFlowException, OAuth2ServiceException {
+		OAuth2TokenResponse mockedResponse = mockRetrieveAccessToken();
+
+		OAuth2TokenResponse response = cut.disableCache(true).token(exchangeToken).execute();
+
+		assertThat(response.getAccessToken(), is(mockedResponse.getAccessToken()));
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), any(), any(), any(), eq(true));
 
-		OAuth2TokenResponse jwt = cut.subdomain(subdomain).token(userTokenToBeExchanged).execute();
+		cut.disableCache(false).token(exchangeToken).execute();
 
-		assertThat(jwt.getAccessToken(), is(dummyAccessToken.getAccessToken()));
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), any(), any(), any(), eq(false));
 	}
 
 	@Test
 	public void execute_withAdditionalAuthorities() throws TokenFlowException, OAuth2ServiceException {
+		OAuth2TokenResponse mockedResponse = mockRetrieveAccessToken();
+
 		Map<String, String> additionalAuthorities = new HashMap<String, String>();
 		additionalAuthorities.put("DummyAttribute", "DummyAttributeValue");
-
 		Map<String, String> additionalAuthoritiesParam = new HashMap<>();
 		additionalAuthoritiesParam.put("authorities", "{\"az_attr\":{\"DummyAttribute\":\"DummyAttributeValue\"}}");
 
-		when(mockTokenService
-				.retrieveAccessTokenViaJwtBearerTokenGrant(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
-						eq(userTokenToBeExchanged),
-						isNull(), eq(additionalAuthoritiesParam)))
-								.thenReturn(dummyAccessToken);
-
-		OAuth2TokenResponse jwt = cut.token(userTokenToBeExchanged)
+		OAuth2TokenResponse actualResponse = cut.token(exchangeToken)
 				.attributes(additionalAuthorities)
 				.execute();
 
-		assertThat(jwt.getAccessToken(), is(dummyAccessToken.getAccessToken()));
+		assertThat(actualResponse.getAccessToken(), is(mockedResponse.getAccessToken()));
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaJwtBearerTokenGrant(eq(TOKEN_ENDPOINT_URI), eq(clientCredentials),
+						eq(exchangeToken),
+						isNull(), eq(additionalAuthoritiesParam), anyBoolean());
+	}
+
+	private OAuth2TokenResponse mockRetrieveAccessToken() throws OAuth2ServiceException {
+		OAuth2TokenResponse tokenResponse = new OAuth2TokenResponse("4bfad399ca10490da95c2b5eb4451d53",
+				441231, REFRESH_TOKEN);
+		when(mockTokenService.retrieveAccessTokenViaJwtBearerTokenGrant(eq(TOKEN_ENDPOINT_URI),
+				eq(clientCredentials),
+				eq(exchangeToken),
+				any(), any(), anyBoolean()))
+						.thenReturn(tokenResponse);
+		return tokenResponse;
 	}
 
 }
\ No newline at end of file