Repository: SAP/cloud-security-xsuaa-integration
Pull request #437 (2020-12-23 09:59:16)

token-client enhancement with X-zid header


Patch:
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 02ef210f..c6bc0829 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
@@ -40,7 +40,7 @@ public class IasXsuaaExchangeBroker {
 				new DefaultOAuth2TokenService(httpClient),
 				new XsuaaDefaultEndpoints(serviceConfiguration.getUrl()),
 				new ClientCredentials(serviceConfiguration.getClientId(), serviceConfiguration.getClientSecret()));
-		OAuth2TokenResponse tokenResponse = tokenFlows.userTokenFlow().token(token.getTokenValue()).execute();
+		OAuth2TokenResponse tokenResponse = tokenFlows.userTokenFlow().token(token).execute();
 		logger.debug("Response token from Ias to Xsuaa token exchange {}", tokenResponse.getAccessToken());
 		return tokenResponse.getAccessToken();
 	}
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasToken.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasToken.java
new file mode 100644
index 00000000..b17791f2
--- /dev/null
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/IasToken.java
@@ -0,0 +1,107 @@
+package com.sap.cloud.security.xsuaa.extractor;
+
+import com.sap.cloud.security.config.Service;
+import com.sap.cloud.security.json.JsonObject;
+import com.sap.cloud.security.token.Token;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.security.Principal;
+import java.time.Instant;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import static com.sap.cloud.security.token.TokenClaims.*;
+
+class IasToken implements Token {
+
+	private Jwt decodedToken;
+
+	public IasToken(Jwt jwt) {
+		this.decodedToken = jwt;
+	}
+
+	@Nullable
+	@Override
+	public String getHeaderParameterAsString(@Nonnull String headerName) {
+		return decodedToken.getHeaders().get(headerName).toString();
+	}
+
+	@Override
+	public boolean hasHeaderParameter(@Nonnull String headerName) {
+		return decodedToken.getHeaders().containsValue(headerName);
+	}
+
+	@Override
+	public boolean hasClaim(@Nonnull String claimName) {
+		return decodedToken.containsClaim(claimName);
+	}
+
+	@Nullable
+	@Override
+	public String getClaimAsString(@Nonnull String claimName) {
+		return decodedToken.getClaimAsString(claimName);
+	}
+
+	@Override
+	public List<String> getClaimAsStringList(@Nonnull String claimName) {
+		return decodedToken.getClaimAsStringList(claimName);
+	}
+
+	@Nullable
+	@Override
+	public JsonObject getClaimAsJsonObject(@Nonnull String claimName) {
+		return decodedToken.getClaim(claimName);
+	}
+
+	@Nullable
+	@Override
+	public Instant getExpiration() {
+		return decodedToken.getExpiresAt();
+	}
+
+	@Override
+	public boolean isExpired() {
+		return Objects.requireNonNull(decodedToken.getExpiresAt(), "Token expiration time is missing")
+				.isBefore(Instant.now());
+	}
+
+	@Nullable
+	@Override
+	public Instant getNotBefore() {
+		return decodedToken.getNotBefore();
+	}
+
+	@Override
+	public String getTokenValue() {
+		return decodedToken.getTokenValue();
+	}
+
+	@Override
+	public Principal getPrincipal() {
+		return null;
+	}
+
+	@Override
+	public Service getService() {
+		return Service.IAS;
+	}
+
+	@Override
+	public Set<String> getAudiences() {
+		return new LinkedHashSet<>(getClaimAsStringList(AUDIENCE));
+	}
+
+	@Override
+	public String getZoneId() {
+		return decodedToken.getClaimAsString(SAP_GLOBAL_ZONE_ID);
+	}
+
+	@Override
+	public String getClientId() {
+		return decodedToken.getClaimAsString(SAP_GLOBAL_USER_ID);
+	}
+}
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 4c3074f4..f86ecb42 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
@@ -1,5 +1,6 @@
 package com.sap.cloud.security.xsuaa.extractor;
 
+import com.sap.cloud.security.token.Token;
 import com.sap.cloud.security.token.TokenClaims;
 import com.sap.cloud.security.xsuaa.jwt.Base64JwtDecoder;
 import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
@@ -55,11 +56,11 @@ public class IasXsuaaExchangeBroker {
 	 * Request a Xsuaa token using Ias token as a grant.
 	 *
 	 * @param iasToken
-	 *            encoded IAS token
+	 *            IAS token
 	 * @return encoded Xsuaa token
 	 */
 	@Nullable
-	public String getXsuaaToken(String iasToken) {
+	public String getXsuaaToken(Token iasToken) {
 		try {
 			return xsuaaTokenFlows.userTokenFlow().token(iasToken).execute().getAccessToken();
 		} catch (TokenFlowException e) {
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java
index 354bc631..56fd2249 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java
@@ -1,5 +1,8 @@
 package com.sap.cloud.security.xsuaa.extractor;
 
+import com.nimbusds.jwt.JWT;
+import com.nimbusds.jwt.JWTParser;
+import com.sap.cloud.security.token.Token;
 import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
 import com.sap.cloud.security.xsuaa.client.ClientCredentials;
 import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;
@@ -14,6 +17,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.cache.Cache;
 import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
 import org.springframework.util.StringUtils;
 import org.springframework.web.client.RestTemplate;
@@ -22,6 +26,7 @@ import javax.servlet.http.HttpServletRequest;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.List;
@@ -181,10 +186,20 @@ public class TokenBrokerResolver implements BearerTokenResolver {
 			if (oAuth2token == null) {
 				break;
 			}
-			if (iasXsuaaExchangeBroker.isXsuaaToken(oAuth2token) || !iasXsuaaExchangeBroker.isIasXsuaaXchangeEnabled()) {
+			if (iasXsuaaExchangeBroker.isXsuaaToken(oAuth2token)
+					|| !iasXsuaaExchangeBroker.isIasXsuaaXchangeEnabled()) {
 				return oAuth2token;
 			} else if (iasXsuaaExchangeBroker.isIasXsuaaXchangeEnabled()) {
-				return iasXsuaaExchangeBroker.getXsuaaToken(oAuth2token);
+				try {
+					JWT decodedToken = JWTParser.parse(oAuth2token);
+					Jwt jwt = new Jwt(oAuth2token, decodedToken.getJWTClaimsSet().getIssueTime().toInstant(),
+							decodedToken.getJWTClaimsSet().getExpirationTime().toInstant(),
+							decodedToken.getHeader().toJSONObject(), decodedToken.getJWTClaimsSet().getClaims());
+					Token token = new IasToken(jwt);
+					return iasXsuaaExchangeBroker.getXsuaaToken(token);
+				} catch (ParseException e) {
+					logger.error("Couldn't decode the token: {}", e.getMessage());
+				}
 			}
 			break;
 		case BASIC:
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 d8437ba2..13653aaa 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
@@ -187,6 +187,33 @@ public abstract class AbstractOAuth2TokenService implements OAuth2TokenService,
 		return getOAuth2TokenResponse(tokenEndpoint, headers, parameters, subdomain, disableCacheForRequest);
 	}
 
+	@Override
+	public OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(URI tokenEndpoint,
+			ClientCredentials clientCredentials, @Nonnull String token,
+			@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest,
+			@Nonnull String zoneId)
+			throws OAuth2ServiceException {
+		assertNotNull(tokenEndpoint, "tokenEndpoint is required");
+		assertNotNull(clientCredentials, "clientCredentials are required");
+		assertNotNull(token, "token is required");
+		assertNotNull(zoneId, "ZoneId is required to create X-zid header");
+
+		Map<String, String> parameters = new RequestParameterBuilder()
+				.withGrantType(GRANT_TYPE_JWT_BEARER)
+				.withClientCredentials(clientCredentials)
+				.withToken(token)
+				.withOptionalParameters(optionalParameters)
+				.buildAsMap();
+
+		HttpHeaders headers = HttpHeadersFactory.createWithoutAuthorizationHeader().withHeader(HttpHeaders.X_ZID,
+				zoneId);
+
+		if (isCacheDisabled() || disableCacheForRequest) {
+			return requestAccessToken(tokenEndpoint, headers, parameters);
+		}
+		return getOrRequestAccessToken(tokenEndpoint, headers, parameters);
+	}
+
 	/**
 	 * Implements the HTTP client specific logic to perform an HTTP request and
 	 * handle the response.
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 e6e97913..35c161a5 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
@@ -210,4 +210,29 @@ public interface OAuth2TokenService {
 				optionalParameters, false);
 	}
 
+	/**
+	 * @param tokenEndpointUri
+	 *            the token endpoint URI.
+	 * @param clientCredentials
+	 *            the client id and secret of the OAuth client, the recipient of the
+	 *            token.
+	 * @param token
+	 *            the JWT token identifying representing the user to be
+	 *            authenticated
+	 * @param optionalParameters
+	 *            optional request parameters, can be null.
+	 * @param disableCache
+	 *            setting to true disables the token cache for this request.
+	 * @param xZid
+	 *            zone id of the tenant
+	 * @return the OAuth2AccessToken
+	 * @throws OAuth2ServiceException
+	 *             in case of an error during the http request.
+	 */
+	OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(URI tokenEndpointUri,
+			ClientCredentials clientCredentials,
+			@Nonnull String token,
+			@Nullable Map<String, String> optionalParameters,
+			boolean disableCache,
+			@Nonnull String xZid) throws OAuth2ServiceException;
 }
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java
index c0b683bf..0429ade6 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/http/HttpHeaders.java
@@ -8,6 +8,7 @@ public class HttpHeaders {
 	public static final String AUTHORIZATION = "Authorization";
 	public static final String ACCEPT = "Accept";
 	public static final String CONTENT_TYPE = "Content-Type";
+	public static final String X_ZID = "X-zid";
 
 	private final Set<HttpHeader> headers;
 
@@ -23,6 +24,11 @@ public class HttpHeaders {
 		return headers;
 	}
 
+	public HttpHeaders withHeader(String headerName, String value) {
+		this.headers.add(new HttpHeader(headerName, value));
+		return this;
+	}
+
 	@Override
 	public boolean equals(Object o) {
 		if (this == o)
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 ac588a4e..f3128c1d 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
@@ -13,6 +13,7 @@ import static com.sap.cloud.security.xsuaa.Assertions.assertNotNull;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import com.sap.cloud.security.token.Token;
 import com.sap.cloud.security.xsuaa.Assertions;
 import com.sap.cloud.security.xsuaa.client.*;
 import com.sap.xsa.security.container.XSTokenRequest;
@@ -27,6 +28,7 @@ public class UserTokenFlow {
 
 	private XsuaaTokenFlowRequest request;
 	private String token;
+	private String xZid;
 	private OAuth2TokenService tokenService;
 	private boolean disableCache = false;
 	private List<String> scopes = new ArrayList<>();
@@ -57,13 +59,27 @@ public class UserTokenFlow {
 	/**
 	 * Sets the JWT token that should be exchanged for another JWT token.
 	 *
-	 * @param token
+	 * @param encodedToken
 	 *            - the JWT token.
 	 * @return this builder object.
 	 */
-	public UserTokenFlow token(String token) {
-		assertNotNull(token, "Token must not be null.");
-		this.token = token;
+	public UserTokenFlow token(String encodedToken) {
+		assertNotNull(encodedToken, "Token must not be null.");
+		this.token = encodedToken;
+		return this;
+	}
+
+	/**
+	 * Sets the JWT token that should be exchanged for another JWT token.
+	 *
+	 * @param token
+	 *            - decoded JWT token.
+	 * @return this builder object.
+	 */
+	public UserTokenFlow token(Token token) {
+		assertNotNull(token, "Encoded token must not be null.");
+		this.token = token.getTokenValue();
+		this.xZid = token.getZoneId();
 		return this;
 	}
 
@@ -188,10 +204,17 @@ public class UserTokenFlow {
 		}
 
 		try {
-			return tokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
-					request.getTokenEndpoint(),
-					new ClientCredentials(request.getClientId(), request.getClientSecret()),
-					token, request.getSubdomain(), optionalParameter, disableCache);
+			if (xZid == null) {
+				return tokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
+						request.getTokenEndpoint(),
+						new ClientCredentials(request.getClientId(), request.getClientSecret()),
+						token, request.getSubdomain(), optionalParameter, disableCache);
+			} else {
+				return tokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
+						request.getTokenEndpoint(),
+						new ClientCredentials(request.getClientId(), request.getClientSecret()),
+						token, optionalParameter, disableCache, xZid);
+			}
 		} catch (OAuth2ServiceException e) {
 			throw new TokenFlowException(
 					String.format(


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 c6574d82..dbb16dba 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
@@ -537,7 +537,7 @@ public class XSUserInfoAdapterTest {
 		UserTokenFlow userTokenFlowMock = mock(UserTokenFlow.class);
 		when(userTokenFlowMock.subdomain(any())).thenReturn(userTokenFlowMock);
 		when(userTokenFlowMock.attributes(any())).thenReturn(userTokenFlowMock);
-		when(userTokenFlowMock.token(any())).thenReturn(userTokenFlowMock);
+		when(userTokenFlowMock.token(anyString())).thenReturn(userTokenFlowMock);
 		when(userTokenFlowMock.execute()).thenReturn(Mockito.mock(OAuth2TokenResponse.class));
 		return userTokenFlowMock;
 	}
diff --git a/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolverTest.java b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolverTest.java
index f4aa4781..ff82b196 100644
--- a/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolverTest.java
+++ b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolverTest.java
@@ -25,7 +25,7 @@ public class TokenBrokerResolverTest {
 	private MockHttpServletRequest request;
 	private static TokenBrokerResolver tokenBroker;
 	private static final String XSUAA_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHRfYXR0ciI6eyJlbmhhbmNlciI6IlhTVUFBIn19._cocFCqqATDXx6eBUoF22W9F8VwUVYY59XdLGdEDFso";
-	private static final String IAS_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJncmFudF90eXBlIjoiY2xpZW50X2NyZWRlbnRpYWxzIiwidXNlcl9pZCI6IjEwMDIxOTEiLCJ1c2VyX25hbWUiOiJXT0xGR0FORyIsImVtYWlsIjoiV09MRkdBTkdAdW5rbm93biIsImlhdCI6MTQ0MjkxMjI0NCwiZXhwIjoxNDQyOTU1MzIyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiZXh0X2F0dHIiOnsic2VydmljZWluc3RhbmNlaWQiOiJhYmNkMTIzNCJ9fQ.Nc4q_soq4b843MlIofg79UpZThJis3JQ_QRSc7T3bXY";
+	private static final String IAS_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJncmFudF90eXBlIjoiY2xpZW50X2NyZWRlbnRpYWxzIiwidXNlcl9pZCI6IjEwMDIxOTEiLCJ1c2VyX25hbWUiOiJXT0xGR0FORyIsImVtYWlsIjoiV09MRkdBTkdAdW5rbm93biIsImlhdCI6MTQ0MjkxMjI0NCwiZXhwIjoxNDQyOTU1MzIyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiZXh0X2F0dHIiOnsic2VydmljZWluc3RhbmNlaWQiOiJhYmNkMTIzNCJ9LCJ6aWQiOiJ6b25lSWQifQ.uzdP2WyfullaZ_5aSjMr2shTfyqqXxbfgShjtfPjigY";
 
 	@Before
 	public void setUp() {
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 6536f9f8..1db329cb 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
@@ -1,11 +1,9 @@
-/**
- * 
- */
 package com.sap.cloud.security.xsuaa.extractor;
 
 import java.net.URI;
 import java.util.Map;
 
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.springframework.cache.Cache;
@@ -95,6 +93,15 @@ public class TokenBrokerTestConfiguration {
 					throws OAuth2ServiceException {
 				return new OAuth2TokenResponse(XSUAA_TOKEN, 100, null);
 			}
+
+			@Override
+			public OAuth2TokenResponse retrieveAccessTokenViaJwtBearerTokenGrant(URI tokenEndpointUri,
+					ClientCredentials clientCredentials, @Nonnull String token,
+					@Nullable Map<String, String> optionalParameters, boolean disableCacheForRequest,
+					@Nonnull String xZidHeader)
+					throws OAuth2ServiceException {
+				return new OAuth2TokenResponse(XSUAA_TOKEN, 100, null);
+			}
 		};
 	}
 
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/extractor/IasTokenTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/extractor/IasTokenTest.java
new file mode 100644
index 00000000..864e1f9c
--- /dev/null
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/extractor/IasTokenTest.java
@@ -0,0 +1,55 @@
+package com.sap.cloud.security.xsuaa.extractor;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.sap.cloud.security.config.Service;
+import com.sap.cloud.security.xsuaa.test.JwtGenerator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.util.Arrays;
+import java.util.Date;
+
+import static com.sap.cloud.security.token.TokenClaims.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+class IasTokenTest {
+
+	private JWTClaimsSet.Builder claimsSetBuilder = null;
+	private final String zoneId = "e2f7fbdb-0326-40e6-940f-dfddad057ff3";
+	private static final String USER_ID = "test-user-id";
+	private static final String USER_NAME_VALUE = "testUser";
+
+	@BeforeEach
+	void setUp() {
+		claimsSetBuilder = new JWTClaimsSet.Builder()
+				.issueTime(new Date())
+				.expirationTime(JwtGenerator.NO_EXPIRE_DATE)
+				.claim(SAP_GLOBAL_USER_ID, USER_ID)
+				.claim(SAP_GLOBAL_ZONE_ID, zoneId)
+				.claim(USER_NAME, USER_NAME_VALUE)
+				.claim(AUDIENCE, Arrays.asList("aud1", "aud2"));
+	}
+
+	@Test
+	public void checkIasTokenTest() {
+		IasToken token = createToken(claimsSetBuilder);
+
+		assertThat(token.getClientId(), is(USER_ID));
+		assertThat(token.getZoneId(), is(zoneId));
+		assertThat(token.getExpiration(), is(JwtGenerator.NO_EXPIRE_DATE.toInstant()));
+		assertThat(token.getAudiences().size(), is(2));
+		assertThat(token.getService(), is(Service.IAS));
+		assertThat(token.getTokenValue(), not(""));
+		assertThat(token.getTokenValue(), notNullValue());
+		assertThat(token.isExpired(), is(false));
+		assertThat(token.hasClaim(EMAIL), is(false));
+		assertThat(token.hasClaim(SAP_GLOBAL_ZONE_ID), is(true));
+	}
+
+	private IasToken createToken(JWTClaimsSet.Builder claimsBuilder) {
+		Jwt jwt = JwtGenerator.createFromClaims(claimsBuilder.build());
+		return new IasToken(jwt);
+	}
+}
\ No newline at end of file
diff --git a/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersFactoryTest.java b/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersFactoryTest.java
index 0f5c0330..dc3c360d 100644
--- a/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersFactoryTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/xsuaa/http/HttpHeadersFactoryTest.java
@@ -11,6 +11,7 @@ public class HttpHeadersFactoryTest {
 	private static final HttpHeader ACCEPT_JSON_HEADER = new HttpHeader(ACCEPT, APPLICATION_JSON.value());
 	private static final HttpHeader CONTENT_TYPE_URL_ENCODED = new HttpHeader(CONTENT_TYPE,
 			APPLICATION_FORM_URLENCODED.value());
+	private static final HttpHeader X_ZID = new HttpHeader(HttpHeaders.X_ZID, "zoneId");
 	private static final String TOKEN = "TOKEN CONTENT";
 
 	@Test
@@ -29,4 +30,12 @@ public class HttpHeadersFactoryTest {
 		assertThat(headers.getHeaders()).contains(tokenHeader);
 	}
 
+	@Test
+	public void createWithXzidHeader() {
+		HttpHeaders headers = HttpHeadersFactory.createWithoutAuthorizationHeader().withHeader(HttpHeaders.X_ZID,
+				"zoneId");
+		assertThat(headers.getHeaders()).hasSize(3);
+		assertThat(headers.getHeaders()).containsExactlyInAnyOrder(X_ZID, ACCEPT_JSON_HEADER, CONTENT_TYPE_URL_ENCODED);
+	}
+
 }
\ No newline at end of file
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 b312ba65..bb296e08 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
@@ -1,29 +1,22 @@
 package com.sap.cloud.security.xsuaa.tokenflows;
 
-import static com.sap.cloud.security.xsuaa.tokenflows.TestConstants.*;
-import static java.util.Collections.*;
-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.*;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
-
-import java.util.HashMap;
-import java.util.Map;
-
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.xsuaa.client.*;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.junit.MockitoJUnitRunner;
 
-import com.sap.cloud.security.xsuaa.client.ClientCredentials;
-import com.sap.cloud.security.xsuaa.client.OAuth2TokenResponse;
-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 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 {
@@ -163,6 +156,33 @@ public class UserTokenFlowTest {
 						isNull(), eq(additionalAuthoritiesParam), anyBoolean());
 	}
 
+	@Test
+	public void execute_withXzidHeader() throws TokenFlowException, OAuth2ServiceException {
+		Token mockedToken = mock(Token.class);
+		OAuth2TokenResponse mockedResponse = new OAuth2TokenResponse("4bfad399ca10490da95c2b5eb4451d53",
+				441231, REFRESH_TOKEN);
+
+		when(mockedToken.getTokenValue()).thenReturn("encoded.Token.Value");
+		when(mockedToken.getZoneId()).thenReturn("zone");
+		when(mockTokenService.retrieveAccessTokenViaJwtBearerTokenGrant(
+				eq(TOKEN_ENDPOINT_URI),
+				eq(clientCredentials),
+				eq("encoded.Token.Value"),
+				anyMap(), anyBoolean(), eq("zone")))
+						.thenReturn(mockedResponse);
+
+		OAuth2TokenResponse actualResponse = cut.token(mockedToken)
+				.execute();
+
+		assertThat(actualResponse.getAccessToken()).isSameAs(mockedResponse.getAccessToken());
+		verify(mockTokenService, times(1))
+				.retrieveAccessTokenViaJwtBearerTokenGrant(
+						eq(TOKEN_ENDPOINT_URI),
+						eq(clientCredentials),
+						eq("encoded.Token.Value"),
+						anyMap(), anyBoolean(), eq("zone"));
+	}
+
 	private OAuth2TokenResponse mockRetrieveAccessToken() throws OAuth2ServiceException {
 		OAuth2TokenResponse tokenResponse = new OAuth2TokenResponse("4bfad399ca10490da95c2b5eb4451d53",
 				441231, REFRESH_TOKEN);