Repository: SAP/cloud-security-xsuaa-integration
Pull request #1135 (2023-04-03 15:29:45)

Add JwtBearerTokenFlow to 2.x and Deprecate UserTokenFlow


Patch:
diff --git a/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java b/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
index d3249a7c..99d32cf4 100644
--- a/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
+++ b/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
@@ -492,7 +492,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	private String performUserTokenFlow(XsuaaTokenFlows xsuaaTokenFlows, Map<String, String> additionalAuthAttributes) {
 		String userToken;
 		try {
-			userToken = xsuaaTokenFlows.userTokenFlow()
+			userToken = xsuaaTokenFlows.jwtBearerTokenFlow()
 					.subdomain(getSubdomain())
 					.token(getAppToken())
 					.attributes(additionalAuthAttributes)
diff --git a/java-security/src/main/java/com/sap/cloud/security/servlet/IasXsuaaExchangeBroker.java b/java-security/src/main/java/com/sap/cloud/security/servlet/IasXsuaaExchangeBroker.java
index 12990129..f6a5fdb5 100644
--- a/java-security/src/main/java/com/sap/cloud/security/servlet/IasXsuaaExchangeBroker.java
+++ b/java-security/src/main/java/com/sap/cloud/security/servlet/IasXsuaaExchangeBroker.java
@@ -52,7 +52,7 @@ class IasXsuaaExchangeBroker {
 	 */
 	@Nullable
 	public String resolve(Token token) throws TokenFlowException, HttpClientException {
-		OAuth2TokenResponse tokenResponse = xsuaaTokenFlows.userTokenFlow().token(token).execute();
+		OAuth2TokenResponse tokenResponse = xsuaaTokenFlows.jwtBearerTokenFlow().token(token).execute();
 		LOGGER.debug("Response token from Ias to Xsuaa token exchange {}", tokenResponse.getDecodedAccessToken().getPayload());
 		return tokenResponse.getAccessToken();
 	}
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasXsuaaExchangeBroker.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasXsuaaExchangeBroker.java
index f7e8225f..ff87007a 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasXsuaaExchangeBroker.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasXsuaaExchangeBroker.java
@@ -92,7 +92,7 @@ public class IasXsuaaExchangeBroker implements BearerTokenResolver {
 	@Nullable
 	String doIasXsuaaXchange(DecodedJwt decodedJwt) {
 		try {
-			return xsuaaTokenFlows.userTokenFlow().token(createToken(decodedJwt)).execute().getAccessToken();
+			return xsuaaTokenFlows.jwtBearerTokenFlow().token(decodedJwt.getEncodedToken()).execute().getAccessToken();
 		} catch (TokenFlowException e) {
 			logger.error("Xsuaa token request failed {}", e.getMessage());
 		}
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlow.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlow.java
new file mode 100644
index 00000000..16833c97
--- /dev/null
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlow.java
@@ -0,0 +1,183 @@
+package com.sap.cloud.security.xsuaa.tokenflows;
+
+import com.sap.cloud.security.config.ClientIdentity;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.xsuaa.Assertions;
+import com.sap.cloud.security.xsuaa.client.*;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.sap.cloud.security.xsuaa.Assertions.assertNotNull;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.AUTHORITIES;
+import static com.sap.cloud.security.xsuaa.client.OAuth2TokenServiceConstants.SCOPE;
+import static com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlowsUtils.buildAdditionalAuthoritiesJson;
+
+/**
+ * A JWT bearer token flow builder. <br>
+ * Applications can use this flow to exchange a given user token for a new JWT token.
+ */
+public class JwtBearerTokenFlow {
+
+    private final OAuth2TokenService tokenService;
+    private final OAuth2ServiceEndpointsProvider endpointsProvider;
+    private final ClientIdentity clientIdentity;
+    private final Map<String, String> optionalParameters = new HashMap<>();
+
+    private String bearerToken;
+    private String xZid;
+    private List<String> scopes = new ArrayList<>();
+    private String subdomain;
+    private boolean disableCache;
+
+    public JwtBearerTokenFlow(@Nonnull OAuth2TokenService tokenService,
+                              @Nonnull OAuth2ServiceEndpointsProvider endpointsProvider,
+                              @Nonnull ClientIdentity clientIdentity) {
+        Assertions.assertNotNull(tokenService, "OAuth2TokenService must not be null!");
+        Assertions.assertNotNull(endpointsProvider, "OAuth2ServiceEndpointsProvider must not be null!");
+        Assertions.assertNotNull(clientIdentity, "ClientIdentity must not be null!");
+
+        this.tokenService = tokenService;
+        this.endpointsProvider = endpointsProvider;
+        this.clientIdentity = clientIdentity;
+    }
+
+    /**
+     * Sets the bearer token that should be exchanged for another JWT token.
+     *
+     * @param bearerToken
+     *            - the bearer token.
+     * @return this builder.
+     */
+    public JwtBearerTokenFlow token(@Nonnull String bearerToken) {
+        assertNotNull(bearerToken, "Bearer token must not be null.");
+        this.bearerToken = bearerToken;
+        return this;
+    }
+
+    /**
+     * Sets the JWT token that should be exchanged for another JWT token.
+     * This setter also extracts the zid(zone id) claim from the token and
+     * sets it in the X-zid header, therefore {@link JwtBearerTokenFlow#zoneId(String)}} is not required to be used.
+     *
+     * @param token
+     *            - the Token.
+     * @return this builder.
+     */
+    public JwtBearerTokenFlow token(@Nonnull Token token) {
+        assertNotNull(token, "Token must not be null.");
+        this.bearerToken = token.getTokenValue();
+        this.xZid = token.getZoneId();
+        return this;
+    }
+
+    /**
+     * Sets the zoneid of the tenant<br>
+     *
+     * @param zoneId
+     *            - the zoneId.
+     * @return this builder.
+     */
+    public JwtBearerTokenFlow zoneId(String zoneId) {
+        this.xZid = zoneId;
+        return this;
+    }
+
+    /**
+     * Sets the scope attribute for the token request. This will restrict the scope
+     * of the created token to the scopes provided. By default the scope is not
+     * restricted and the created token contains all granted scopes.
+     *
+     * If you specify a scope that is not authorized for the user, the token request
+     * will fail.
+     *
+     * @param scopes
+     *            - one or many scopes as string.
+     * @return this builder.
+     */
+    public JwtBearerTokenFlow scopes(@Nonnull String... scopes) {
+        Assertions.assertNotNull(scopes, "Scopes must not be null!");
+        this.scopes = Arrays.asList(scopes);
+        return this;
+    }
+
+    /**
+     * Set the Subdomain the token is requested for.
+     *
+     * @param subdomain
+     *            - the subdomain.
+     * @return this builder.
+     */
+    public JwtBearerTokenFlow subdomain(String subdomain) {
+        this.subdomain = subdomain;
+        return this;
+    }
+
+    /**
+     * Adds additional authorization attributes to the request. <br>
+     * Clients can use this to request additional attributes in the
+     * 'az_attr' claim of the returned token.
+     *
+     * @param additionalAuthorizationAttributes
+     *            - the additional attributes.
+     * @return this builder.
+     */
+    public JwtBearerTokenFlow attributes(Map<String, String> additionalAuthorizationAttributes) {
+        optionalParameters.put(AUTHORITIES, buildAdditionalAuthoritiesJson(additionalAuthorizationAttributes));
+        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 JwtBearerTokenFlow disableCache(boolean disableCache) {
+        this.disableCache = disableCache;
+        return this;
+    }
+
+    /**
+     * Executes this flow against the XSUAA endpoint. As a result the exchanged JWT
+     * token is returned.
+     *
+     * @return the JWT instance returned by XSUAA.
+     * @throws IllegalStateException
+     *             - in case not all mandatory fields of the token flow request have
+     *             been set.
+     * @throws TokenFlowException
+     *             - in case of an error during the flow, or when the token cannot
+     *             be obtained.
+     */
+    public OAuth2TokenResponse execute() throws TokenFlowException {
+        if (bearerToken == null) {
+            throw new IllegalStateException("A bearerToken must be set before executing the flow.");
+        }
+
+        String scopesParameter = String.join(" ", scopes);
+        if (!scopesParameter.isEmpty()) {
+            optionalParameters.put(SCOPE, scopesParameter);
+        }
+
+        try {
+            if (xZid == null) {
+                return tokenService
+                        .retrieveAccessTokenViaJwtBearerTokenGrant(endpointsProvider.getTokenEndpoint(), clientIdentity,
+                                bearerToken, subdomain, optionalParameters, disableCache);
+            }
+            return tokenService
+                    .retrieveAccessTokenViaJwtBearerTokenGrant(endpointsProvider.getTokenEndpoint(), clientIdentity,
+                            bearerToken, optionalParameters, disableCache, xZid);
+        } catch (OAuth2ServiceException e) {
+            throw new TokenFlowException(
+                    String.format("Error requesting user token with grant_type '%s': %s",
+                            OAuth2TokenServiceConstants.GRANT_TYPE_JWT_BEARER, e.getMessage()), e);
+        }
+    }
+}
\ No newline at end of file
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 dd0454e2..dcceaed0 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
@@ -25,7 +25,10 @@ import static com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlowsUtils.build
  * Applications retrieve an instance of this builder from
  * {@link XsuaaTokenFlows} and then create the flow request using a builder
  * pattern.
+ *
+ * @deprecated Please use {@link JwtBearerTokenFlow} instead.
  */
+@Deprecated
 public class UserTokenFlow {
 
 	private final XsuaaTokenFlowRequest request;
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlows.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlows.java
index b302bae8..8af8d91a 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlows.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlows.java
@@ -87,11 +87,24 @@ public class XsuaaTokenFlows implements Serializable {
 	 * endpoints will be derived relative to the base URI.
 	 * 
 	 * @return the {@link UserTokenFlow} builder object.
+	 * @deprecated Please use {@link #jwtBearerTokenFlow()} instead
 	 */
+	@Deprecated
 	public UserTokenFlow userTokenFlow() {
 		return new UserTokenFlow(oAuth2TokenService, endpointsProvider, clientIdentity);
 	}
 
+	/**
+	 * Creates a new JWT Bearer Token Flow builder object.<br>
+	 * Token, authorize and key set endpoints will be derived relative to the base
+	 * URI.
+	 *
+	 * @return the {@link JwtBearerTokenFlow} builder object.
+	 */
+	public JwtBearerTokenFlow jwtBearerTokenFlow() {
+		return new JwtBearerTokenFlow(oAuth2TokenService, endpointsProvider, clientIdentity);
+	}
+
 	/**
 	 * Creates a new Client Credentials Flow builder object. <br>
 	 * Token, authorize and key set endpoints will be derived relative to the base


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
index 05d7985d..35275f5c 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
@@ -15,8 +15,8 @@ import com.sap.cloud.security.json.JsonParsingException;
 import com.sap.cloud.security.token.*;
 import com.sap.cloud.security.xsuaa.client.OAuth2TokenResponse;
 import com.sap.cloud.security.xsuaa.tokenflows.ClientCredentialsTokenFlow;
+import com.sap.cloud.security.xsuaa.tokenflows.JwtBearerTokenFlow;
 import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
-import com.sap.cloud.security.xsuaa.tokenflows.UserTokenFlow;
 import com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlows;
 import com.sap.xsa.security.container.XSUserInfoException;
 import org.apache.commons.io.IOUtils;
@@ -339,8 +339,8 @@ class XSUserInfoAdapterTest {
 		String token = "token";
 
 		XsuaaTokenFlows xsuaaTokenFlowsMock = Mockito.mock(XsuaaTokenFlows.class);
-		UserTokenFlow userTokenFlowMock = userTokenFlowMock();
-		doReturn(userTokenFlowMock).when(xsuaaTokenFlowsMock).userTokenFlow();
+		JwtBearerTokenFlow userTokenFlowMock = jwtBearerTokenFlowMock();
+		doReturn(userTokenFlowMock).when(xsuaaTokenFlowsMock).jwtBearerTokenFlow();
 		cut = createComponentUnderTestSpy();
 		doReturn(xsuaaTokenFlowsMock).when(cut).getXsuaaTokenFlows(anyString(), any(ClientCredentials.class));
 		doReturn(token).when(cut).getAppToken();
@@ -350,7 +350,7 @@ class XSUserInfoAdapterTest {
 		verify(cut, times(1)).getXsuaaTokenFlows(eq(uaaBaseUrl), eq(new ClientCredentials(clientId, clientSecret)));
 		verify(cut, times(1)).getAppToken();
 		verify(userTokenFlowMock, times(1)).token(token);
-		verify(xsuaaTokenFlowsMock, times(1)).userTokenFlow();
+		verify(xsuaaTokenFlowsMock, times(1)).jwtBearerTokenFlow();
 	}
 
 	@Test
@@ -608,8 +608,8 @@ class XSUserInfoAdapterTest {
 		return mockToken;
 	}
 
-	private UserTokenFlow userTokenFlowMock() throws TokenFlowException {
-		UserTokenFlow userTokenFlowMock = mock(UserTokenFlow.class);
+	private JwtBearerTokenFlow jwtBearerTokenFlowMock() throws TokenFlowException {
+		JwtBearerTokenFlow userTokenFlowMock = mock(JwtBearerTokenFlow.class);
 		when(userTokenFlowMock.subdomain(any())).thenReturn(userTokenFlowMock);
 		when(userTokenFlowMock.attributes(any())).thenReturn(userTokenFlowMock);
 		when(userTokenFlowMock.token(anyString())).thenReturn(userTokenFlowMock);
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/JwtBearerTokenFlowTest.java
similarity index 72%
rename from token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/UserTokenFlowTest.java
rename to token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/JwtBearerTokenFlowTest.java
index f70d2e99..a2362746 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/JwtBearerTokenFlowTest.java
@@ -14,53 +14,44 @@ import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
 import org.mockito.junit.MockitoJUnitRunner;
 
 import java.util.HashMap;
 import java.util.Map;
 
 import static com.sap.cloud.security.xsuaa.tokenflows.TestConstants.*;
-import static java.util.Collections.emptyMap;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
 @RunWith(MockitoJUnitRunner.class)
-public class UserTokenFlowTest {
+public class JwtBearerTokenFlowTest {
 
 	private OAuth2TokenService mockTokenService;
-
 	private final String exchangeToken = "exchange token";
 	private final ClientIdentity clientIdentity = new ClientCredentials("clientId", "clientSecret");
 	private OAuth2ServiceEndpointsProvider endpointsProvider;
 
-	private UserTokenFlow cut;
+	private JwtBearerTokenFlow cut;
 
 	@Before
 	public void setup() {
-		OAuth2ServiceConfiguration oAuth2ServiceConfiguration = Mockito.mock(OAuth2ServiceConfiguration.class);
-		Mockito.when(oAuth2ServiceConfiguration.getUrl()).thenReturn(XSUAA_BASE_URI);
+		OAuth2ServiceConfiguration oAuth2ServiceConfiguration = mock(OAuth2ServiceConfiguration.class);
+		when(oAuth2ServiceConfiguration.getUrl()).thenReturn(XSUAA_BASE_URI);
 
 		this.endpointsProvider = new XsuaaDefaultEndpoints(oAuth2ServiceConfiguration);
 		this.mockTokenService = mock(OAuth2TokenService.class);
-		this.cut = new UserTokenFlow(mockTokenService, endpointsProvider, clientIdentity);
+		this.cut = new JwtBearerTokenFlow(mockTokenService, endpointsProvider, clientIdentity);
 	}
 
 	@Test
 	public void constructor_throwsOnNullValues() {
-		assertThatThrownBy(() -> {
-			new UserTokenFlow(null, endpointsProvider, clientIdentity);
-		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("OAuth2TokenService");
+		assertThatThrownBy(() -> new JwtBearerTokenFlow(null, endpointsProvider, clientIdentity)).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("OAuth2TokenService");
 
-		assertThatThrownBy(() -> {
-			new UserTokenFlow(mockTokenService, null, clientIdentity);
-		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("OAuth2ServiceEndpointsProvider");
+		assertThatThrownBy(() -> new JwtBearerTokenFlow(mockTokenService, null, clientIdentity)).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("OAuth2ServiceEndpointsProvider");
 
-		assertThatThrownBy(() -> {
-			new UserTokenFlow(mockTokenService, endpointsProvider, null);
-		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("ClientIdentity");
+		assertThatThrownBy(() -> new JwtBearerTokenFlow(mockTokenService, endpointsProvider, null)).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("ClientIdentity");
 	}
 
 	@Test
@@ -70,7 +61,7 @@ public class UserTokenFlowTest {
 
 		assertThatThrownBy(cut::execute)
 				.isInstanceOf(IllegalStateException.class)
-				.hasMessageContaining("User token not set");
+				.hasMessageContaining("A bearerToken must be set before executing the flow.");
 	}
 
 	@Test
@@ -82,7 +73,7 @@ public class UserTokenFlowTest {
 		assertThatThrownBy(() -> cut.token(exchangeToken).execute())
 				.isInstanceOf(TokenFlowException.class)
 				.hasMessageContaining(
-						"Error requesting token with grant_type 'urn:ietf:params:oauth:grant-type:jwt-bearer'");
+						"Error requesting user token with grant_type 'urn:ietf:params:oauth:grant-type:jwt-bearer'");
 	}
 
 	@Test
@@ -93,9 +84,9 @@ public class UserTokenFlowTest {
 
 		assertThat(response.getAccessToken()).isSameAs(mockedResponse.getAccessToken());
 		verify(mockTokenService, times(1))
-				.retrieveAccessTokenViaJwtBearerTokenGrant(endpointsProvider.getTokenEndpoint(),
-						clientIdentity, exchangeToken, null,
-						emptyMap(), false);
+				.retrieveAccessTokenViaJwtBearerTokenGrant(eq(endpointsProvider.getTokenEndpoint()),
+						eq(clientIdentity), eq(exchangeToken), isNull(),
+						anyMap(), eq(false));
 	}
 
 	@Test
@@ -124,8 +115,7 @@ public class UserTokenFlowTest {
 						optionalParametersCaptor.capture(), anyBoolean());
 
 		Map<String, String> optionalParameters = optionalParametersCaptor.getValue();
-		assertThat(optionalParameters).containsKey("scope");
-		assertThat(optionalParameters.get("scope")).isEqualTo("scope1 scope2");
+		assertThat(optionalParameters).containsEntry("scope", "scope1 scope2");
 	}
 
 	@Test
@@ -149,11 +139,11 @@ public class UserTokenFlowTest {
 				.retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), any(), any(), any(), eq(false));
 	}
 
-	@Test
+	 @Test
 	public void execute_withAdditionalAuthorities() throws TokenFlowException, OAuth2ServiceException {
 		OAuth2TokenResponse mockedResponse = mockRetrieveAccessToken();
 
-		Map<String, String> additionalAuthorities = new HashMap<String, String>();
+		Map<String, String> additionalAuthorities = new HashMap<>();
 		additionalAuthorities.put("DummyAttribute", "DummyAttributeValue");
 		Map<String, String> additionalAuthoritiesParam = new HashMap<>();
 		additionalAuthoritiesParam.put("authorities", "{\"az_attr\":{\"DummyAttribute\":\"DummyAttributeValue\"}}");
@@ -170,19 +160,25 @@ public class UserTokenFlowTest {
 	}
 
 	@Test
-	public void execute_withXzidHeader() throws TokenFlowException, OAuth2ServiceException {
-		Token mockedToken = mock(Token.class);
-		OAuth2TokenResponse mockedResponse = new OAuth2TokenResponse("4bfad399ca10490da95c2b5eb4451d53",
-				441231, REFRESH_TOKEN);
+	public void execute_withZoneId() throws OAuth2ServiceException, TokenFlowException {
+		String zoneId = "zone";
+		OAuth2TokenResponse mockedResponse = mockRetrieveAccessTokenWithZoneId(zoneId);
 
-		when(mockedToken.getTokenValue()).thenReturn("encoded.Token.Value");
-		when(mockedToken.getZoneId()).thenReturn("zone");
-		when(mockTokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
-				eq(TOKEN_ENDPOINT_URI),
-				eq(clientIdentity),
-				eq("encoded.Token.Value"),
-				anyMap(), anyBoolean(), eq("zone")))
-						.thenReturn(mockedResponse);
+		OAuth2TokenResponse response = cut.zoneId(zoneId).token(exchangeToken).execute();
+
+		assertThat(response.getAccessToken()).isSameAs(mockedResponse.getAccessToken());
+
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaJwtBearerTokenGrant(any(), any(), eq(exchangeToken), anyMap(), eq(false), eq(zoneId));
+	}
+
+	@Test
+	public void execute_withZoneId_fromToken() throws TokenFlowException, OAuth2ServiceException {
+		String zoneId = "zone-x";
+		Token mockedToken = mock(Token.class);
+		when(mockedToken.getTokenValue()).thenReturn(exchangeToken);
+		when(mockedToken.getZoneId()).thenReturn(zoneId);
+		OAuth2TokenResponse mockedResponse = mockRetrieveAccessTokenWithZoneId(zoneId);
 
 		OAuth2TokenResponse actualResponse = cut.token(mockedToken)
 				.execute();
@@ -192,8 +188,23 @@ public class UserTokenFlowTest {
 				.retrieveAccessTokenViaJwtBearerTokenGrant(
 						eq(TOKEN_ENDPOINT_URI),
 						eq(clientIdentity),
-						eq("encoded.Token.Value"),
-						anyMap(), anyBoolean(), eq("zone"));
+						eq(exchangeToken),
+						anyMap(), anyBoolean(), eq(zoneId));
+	}
+
+	private OAuth2TokenResponse mockRetrieveAccessTokenWithZoneId(String zoneId) throws OAuth2ServiceException {
+		OAuth2TokenResponse mockedResponse = new OAuth2TokenResponse("4bfad399ca10490da95c2b5eb4451d53",
+				441231, REFRESH_TOKEN);
+		when(mockTokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
+				eq(TOKEN_ENDPOINT_URI),
+				eq(clientIdentity),
+				eq(exchangeToken),
+				anyMap(),
+				eq(false),
+				eq(zoneId))
+		).thenReturn(mockedResponse);
+
+		return mockedResponse;
 	}
 
 	private OAuth2TokenResponse mockRetrieveAccessToken() throws OAuth2ServiceException {
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlowsTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlowsTest.java
index 5d020a41..de0419dc 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlowsTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/tokenflows/XsuaaTokenFlowsTest.java
@@ -5,8 +5,7 @@
  */
 package com.sap.cloud.security.xsuaa.tokenflows;
 
-import com.sap.cloud.security.client.HttpClientFactory;
-import com.sap.cloud.security.config.CacheConfiguration;
+
 import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
 import com.sap.cloud.security.xsuaa.client.*;
 import org.junit.Before;
@@ -21,7 +20,6 @@ import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
 
 import java.net.URI;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -88,6 +86,12 @@ public class XsuaaTokenFlowsTest {
 		assertNotNull("UserTokenFlow must not be null.", flow);
 	}
 
+	@Test
+	public void startJwtBearerTokenFlow() {
+		JwtBearerTokenFlow flow = cut.jwtBearerTokenFlow();
+		assertNotNull("JwtBearerTokenFlow must not be null.", flow);
+	}
+
 	@Test
 	public void startClientCredentialsFlow() {
 		ClientCredentialsTokenFlow flow = cut.clientCredentialsTokenFlow();