Repository: SAP/cloud-security-xsuaa-integration
Pull request #440 (2020-12-23 15:13:15)

add java-api dependency to spring-xsuaa
support usage of com.sap.cloud.security.token.InvalidTokenException without token-client module #430

liga-oz: Hi @nenaraab,

with the new token xchange implementation it uses many more objects from java-api in `TokenBrokerResolver`, `IasXsuaaExchangeBroker` and `IasToken` classes, for example:

- `com.sap.cloud.security.json.JsonObject`
- `com.sap.cloud.security.config.Service`
- `com.sap.cloud.security.token.Token`
- `com.sap.cloud.security.token.TokenClaims`

Instead of the `InvalidTokenException` could throw 

- `org.springframework.security.oauth2.server.resource.InvalidBearerTokenException` 
- `org.springframework.security.authentication.InsufficientAuthenticationException`
nenaraab: yes, `InvalidBearerTokenException` would be my favorite!

Patch:
diff --git a/java-api/src/main/java/com/sap/cloud/security/token/Token.java b/java-api/src/main/java/com/sap/cloud/security/token/Token.java
index 651b4995..454eb56f 100644
--- a/java-api/src/main/java/com/sap/cloud/security/token/Token.java
+++ b/java-api/src/main/java/com/sap/cloud/security/token/Token.java
@@ -6,6 +6,7 @@ import com.sap.cloud.security.json.JsonParsingException;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import java.io.Serializable;
 import java.security.Principal;
 import java.time.Instant;
 import java.util.List;
@@ -14,7 +15,7 @@ import java.util.Set;
 /**
  * Represents a JSON Web Token (JWT).
  */
-public interface Token {
+public interface Token extends Serializable {
 
 	/**
 	 * Returns the header parameter value as string for the given header parameter
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/AbstractToken.java b/java-security/src/main/java/com/sap/cloud/security/token/AbstractToken.java
index 46f3098b..0251223d 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/AbstractToken.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/AbstractToken.java
@@ -28,7 +28,7 @@ import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
  * header parameters and claims.
  */
 public abstract class AbstractToken implements Token {
-
+	private static final long serialVersionUID = 2204172041950251807L;
 	private static final Logger LOGGER = LoggerFactory.getLogger(AbstractToken.class);
 
 	private final DecodedJwt decodedJwt;
@@ -160,12 +160,14 @@ public abstract class AbstractToken implements Token {
 	}
 
 	@Override
-	public boolean equals(Object o) {
-		if (this == o)
+	public boolean equals(Object obj) {
+		if (this == obj)
 			return true;
-		if (!(o instanceof AbstractToken))
+		if (obj == null)
+			return false;
+		if (!(obj instanceof Token))
 			return false;
-		AbstractToken that = (AbstractToken) o;
+		Token that = (Token) obj;
 		return getTokenValue().equals(that.getTokenValue());
 	}
 
diff --git a/samples/spring-security-xsuaa-usage/src/main/java/sample/spring/xsuaa/SecurityConfiguration.java b/samples/spring-security-xsuaa-usage/src/main/java/sample/spring/xsuaa/SecurityConfiguration.java
index 9e94f1e9..0d874648 100644
--- a/samples/spring-security-xsuaa-usage/src/main/java/sample/spring/xsuaa/SecurityConfiguration.java
+++ b/samples/spring-security-xsuaa-usage/src/main/java/sample/spring/xsuaa/SecurityConfiguration.java
@@ -16,6 +16,8 @@
 package sample.spring.xsuaa;
 
 import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.client.XsuaaOAuth2TokenService;
+import com.sap.cloud.security.xsuaa.extractor.IasXsuaaExchangeBroker;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.convert.converter.Converter;
@@ -53,6 +55,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 				.anyRequest().denyAll()
 			.and()
 				.oauth2ResourceServer()
+				.bearerTokenResolver(new IasXsuaaExchangeBroker(xsuaaServiceConfiguration, new XsuaaOAuth2TokenService()))
 				.jwt()
 				.jwtAuthenticationConverter(getJwtAuthenticationConverter());
 		// @formatter:on
diff --git a/spring-xsuaa/README.md b/spring-xsuaa/README.md
index 5ce32c1b..b6659d93 100644
--- a/spring-xsuaa/README.md
+++ b/spring-xsuaa/README.md
@@ -145,6 +145,34 @@ In detail `com.sap.cloud.security.xsuaa.token.SpringSecurityContext` wraps the S
 
 Note that Spring Security Context is thread-bound and is NOT propagated to child-threads. This [Baeldung tutorial: Spring Security Context Propagation article](https://www.baeldung.com/spring-security-async-principal-propagation) provides more information on how to propagate the context.
 
+### Token Exchange
+In case application is required to do token exchange token-client with all its' transitive dependencies need to be available(shouldn't be excluded) in the project.
+```xml
+<dependency>
+    <groupId>com.sap.cloud.security.xsuaa</groupId>
+    <artifactId>token-client</artifactId>
+</dependency>
+```
+To enable token exchange between IAS and XSUAA system environment variable 'IAS_XSUAA_XCHANGE_ENABLED' needs to be provided and enabled. To enable the exchange set the value to any value except 'false' or empty. The exchange between IAS and Xsuaa is disabled by default.
+
+To use the token exchange with OAuth2 authentication method, in the application's security configuration `bearerTokenResolver` needs to be defined in the following manner:
+```java
+http.authorizeRequests()
+      .antMatchers("/secured/path/**").hasAuthority("application.Read")
+      .anyRequest().denyAll()
+      .and().oauth2ResourceServer()
+      .bearerTokenResolver(new IasXsuaaExchangeBroker(xsuaaServiceConfiguration));
+```
+where:
+- xsuaaServiceConfiguration: configuration properties from environment
+
+For all other supported authentication methods i.e. Basic and Client Credentials, [TokenBrokerResolver](https://github.com/SAP/cloud-security-xsuaa-integration/blob/c5b601521c62ad124cd4d5acaf113d93bf265e8a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java) can be configured as bearerTokenResolver and token exchange between IAS to Xsuaa enabled in the same way as above.
+
+Please check out also our samples:
+- [Spring-security-basic-auth sample](https://github.com/SAP/cloud-security-xsuaa-integration/tree/master/samples/spring-security-basic-auth) for `TokenBrokerResolver` usage with basic authentication method.
+- [Spring-security-xsuaa-usage sample](https://github.com/SAP/cloud-security-xsuaa-integration/tree/master/samples/spring-security-xsuaa-usage) for `IasXsuaaExchangeBroker` usage with OAuth2 authentication method 
+
+
 ## Usage
 
 ### Access user/token information
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 f86ecb42..13874973 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,68 +1,78 @@
 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.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.client.*;
+import com.sap.cloud.security.xsuaa.jwt.DecodedJwt;
 import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
 import com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlows;
 import org.json.JSONException;
-import org.json.JSONObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
+import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
 
 import javax.annotation.Nullable;
-
-import static com.sap.cloud.security.token.TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE_ENHANCER;
+import javax.servlet.http.HttpServletRequest;
 
 /**
  * IAS token and XSUAA token exchange and resolution class. Can be used to
  * distinguish between IAS and XSUAA tokens. Controls token exchange between IAS
  * and XSUAA by using IAS_XSUAA_XCHANGE_ENABLED environment variable flag
  */
-public class IasXsuaaExchangeBroker {
+public class IasXsuaaExchangeBroker implements BearerTokenResolver {
 
 	private static final Logger logger = LoggerFactory.getLogger(IasXsuaaExchangeBroker.class);
 
 	private final XsuaaTokenFlows xsuaaTokenFlows;
-	private final boolean isIasXsuaaXchangeEnabled;
-	private static final String IAS_XSUAA_ENABLED = "IAS_XSUAA_XCHANGE_ENABLED";
+	private static final String AUTH_HEADER = "Authorization";
 
 	public IasXsuaaExchangeBroker(XsuaaTokenFlows xsuaaTokenFlows) {
 		this.xsuaaTokenFlows = xsuaaTokenFlows;
-		this.isIasXsuaaXchangeEnabled = resolveIasToXsuaaEnabledFlag();
 	}
 
-	/**
-	 * Verifies if the provided token is Xsuaa token
-	 *
-	 * @param encodedJwtToken
-	 *            Encoded token to be checked
-	 * @return true if provided token is a XSUAA token
-	 */
-	public boolean isXsuaaToken(String encodedJwtToken) {
-		String claims = Base64JwtDecoder.getInstance().decode(encodedJwtToken).getPayload();
+	public IasXsuaaExchangeBroker(XsuaaServiceConfiguration configuration, OAuth2TokenService tokenService) {
+		this.xsuaaTokenFlows = new XsuaaTokenFlows(
+				tokenService,
+				new XsuaaDefaultEndpoints(configuration.getUaaUrl()),
+				new ClientCredentials(configuration.getClientId(), configuration.getClientSecret()));
+	}
+
+	public IasXsuaaExchangeBroker(XsuaaServiceConfiguration configuration) {
+		this(configuration, new DefaultOAuth2TokenService());
+	}
+
+	@Override
+	@Nullable
+	public String resolve(HttpServletRequest request) {
 		try {
-			JSONObject externalAttributeClaim = new JSONObject(claims)
-					.getJSONObject(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE);
-			String externalAttributeValue = externalAttributeClaim
-					.getString(EXTERNAL_ATTRIBUTE_ENHANCER);
-			return externalAttributeValue.equalsIgnoreCase("xsuaa");
+			String oAuth2Token = extractTokenFromRequest(request);
+
+			if (TokenUtil.isIasToXsuaaXchangeEnabled()) {
+				DecodedJwt decodedJwt = TokenUtil.decodeJwt(oAuth2Token);
+				if (!TokenUtil.isXsuaaToken(decodedJwt)) {
+					return doIasXsuaaXchange(decodedJwt);
+				}
+			}
+			return oAuth2Token;
 		} catch (JSONException e) {
-			return false;
+			logger.error("Couldn't decode the token: {}", e.getMessage());
 		}
+		return null;
 	}
 
 	/**
 	 * Request a Xsuaa token using Ias token as a grant.
 	 *
-	 * @param iasToken
-	 *            IAS token
+	 * @param decodedJwt
+	 *            decoded Jwt token
 	 * @return encoded Xsuaa token
 	 */
 	@Nullable
-	public String getXsuaaToken(Token iasToken) {
+	String doIasXsuaaXchange(DecodedJwt decodedJwt) {
 		try {
-			return xsuaaTokenFlows.userTokenFlow().token(iasToken).execute().getAccessToken();
+			return xsuaaTokenFlows.userTokenFlow().token(createToken(decodedJwt)).execute().getAccessToken();
 		} catch (TokenFlowException e) {
 			logger.error("Xsuaa token request failed {}", e.getMessage());
 		}
@@ -70,27 +80,23 @@ public class IasXsuaaExchangeBroker {
 	}
 
 	/**
-	 * Checks value of environment variable 'IAS_XSUAA_XCHANGE_ENABLED'. This value
-	 * determines, whether token exchange between IAS and XSUAA is enabled. If
-	 * IAS_XSUAA_XCHANGE_ENABLED is not provided or with an empty value or with
-	 * value = false, then token exchange is disabled. Any other values are
-	 * interpreted as true.
-	 *
-	 * @return returns true if exchange is enabled and false if disabled
+	 * Resolves the encoded token to Token class
+	 * 
+	 * @param decodedJwt
+	 *            decoded Jwt
+	 * @return IasToken class if provided Jwt token couldn't be parsed
 	 */
-	public boolean isIasXsuaaXchangeEnabled() {
-		return isIasXsuaaXchangeEnabled;
+	private Token createToken(DecodedJwt decodedJwt) {
+		Jwt jwt = TokenUtil.parseJwt(decodedJwt);
+		return new IasToken(jwt);
 	}
 
-	private boolean resolveIasToXsuaaEnabledFlag() {
-		String isEnabled = System.getenv(IAS_XSUAA_ENABLED);
-		logger.debug("System environment variable {} is set to {}", IAS_XSUAA_ENABLED, isEnabled);
-		if (isEnabled != null) {
-			if (!isEnabled.equalsIgnoreCase("false")) {
-				return true;
-			}
+	private String extractTokenFromRequest(HttpServletRequest request) {
+		String authHeader = request.getHeader(AUTH_HEADER);
+
+		if (authHeader != null && authHeader.toLowerCase().startsWith("bearer")) {
+			return authHeader.substring("bearer".length()).trim();
 		}
-		return false;
+		throw new InvalidBearerTokenException("Invalid authorization header");
 	}
-
 }
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 56fd2249..3932cb83 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,7 +1,5 @@
 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;
@@ -9,6 +7,7 @@ import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;
 import com.sap.cloud.security.xsuaa.client.XsuaaDefaultEndpoints;
 import com.sap.cloud.security.xsuaa.client.XsuaaOAuth2TokenService;
 import com.sap.cloud.security.xsuaa.jwt.Base64JwtDecoder;
+import com.sap.cloud.security.xsuaa.jwt.DecodedJwt;
 import com.sap.cloud.security.xsuaa.token.TokenClaims;
 import com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlows;
 import org.json.JSONException;
@@ -17,7 +16,6 @@ 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;
@@ -26,7 +24,6 @@ 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;
@@ -104,7 +101,9 @@ public class TokenBrokerResolver implements BearerTokenResolver {
 				tokenService,
 				new XsuaaDefaultEndpoints(configuration.getUaaUrl()),
 				new ClientCredentials(configuration.getClientId(), configuration.getClientSecret()));
-		this.iasXsuaaExchangeBroker = new IasXsuaaExchangeBroker(this.xsuaaTokenFlows);
+		if (TokenUtil.isIasToXsuaaXchangeEnabled()) {
+			this.iasXsuaaExchangeBroker = new IasXsuaaExchangeBroker(this.xsuaaTokenFlows);
+		}
 	}
 
 	/**
@@ -186,22 +185,17 @@ public class TokenBrokerResolver implements BearerTokenResolver {
 			if (oAuth2token == null) {
 				break;
 			}
-			if (iasXsuaaExchangeBroker.isXsuaaToken(oAuth2token)
-					|| !iasXsuaaExchangeBroker.isIasXsuaaXchangeEnabled()) {
-				return oAuth2token;
-			} else if (iasXsuaaExchangeBroker.isIasXsuaaXchangeEnabled()) {
-				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());
+			if (TokenUtil.isIasToXsuaaXchangeEnabled()) {
+				DecodedJwt decodedJwt = TokenUtil.decodeJwt(oAuth2token);
+				if (!TokenUtil.isXsuaaToken(decodedJwt)) {
+					try {
+						return iasXsuaaExchangeBroker.doIasXsuaaXchange(decodedJwt);
+					} catch (JSONException e) {
+						logger.error("Couldn't decode the token: {}", e.getMessage());
+					}
 				}
 			}
-			break;
+			return oAuth2token;
 		case BASIC:
 			String basicAuthHeader = extractAuthenticationFromHeader(AUTH_BASIC_CREDENTIAL, authHeaderValue);
 			ClientCredentials userCredentialsFromHeader = getCredentialsFromBasicAuthorizationHeader(
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenUtil.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenUtil.java
new file mode 100644
index 00000000..f85a3325
--- /dev/null
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenUtil.java
@@ -0,0 +1,85 @@
+package com.sap.cloud.security.xsuaa.extractor;
+
+import com.sap.cloud.security.xsuaa.jwt.Base64JwtDecoder;
+import com.sap.cloud.security.xsuaa.jwt.DecodedJwt;
+import org.json.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.time.Instant;
+
+/**
+ * Token Utility class to determine provided token type i.e. Xsuaa or IAS, check
+ * token exchange enablement
+ */
+class TokenUtil {
+
+	private static final Logger LOGGER = LoggerFactory.getLogger(TokenUtil.class);
+	private static final String IAS_XSUAA_ENABLED = "IAS_XSUAA_XCHANGE_ENABLED";
+	private static final String EXTERNAL_ATTRIBUTE = "ext_attr";
+	private static final String EXTERNAL_ATTRIBUTE_ENHANCER = "enhancer";
+
+	private TokenUtil() {
+		// use the factory method instead
+	}
+
+	/**
+	 * Splits the bearer token into header, payload and signature.
+	 * 
+	 * @param encodedJwtToken
+	 *            encoded jwt token
+	 * @return DecodedJwt
+	 */
+	static DecodedJwt decodeJwt(String encodedJwtToken) {
+		return Base64JwtDecoder.getInstance().decode(encodedJwtToken);
+	}
+
+	/**
+	 * Parses decoded Jwt token to org.springframework.security.oauth2.jwt
+	 * 
+	 * @param decodedJwt
+	 *            decoded Jwt
+	 * @return Jwt class
+	 */
+	static Jwt parseJwt(DecodedJwt decodedJwt) {
+		JSONObject payload = new JSONObject(decodedJwt.getPayload());
+		JSONObject header = new JSONObject(decodedJwt.getHeader());
+		return new Jwt(decodedJwt.getEncodedToken(), Instant.ofEpochSecond(payload.getLong("iat")),
+				Instant.ofEpochSecond(payload.getLong("exp")),
+				header.toMap(), payload.toMap());
+	}
+
+	/**
+	 * Determines if the provided decoded jwt token is issued by the XSUAA identity
+	 * service.
+	 *
+	 * @param decodedJwt
+	 *            jwt to be checked
+	 * @return true if provided token is a XSUAA token
+	 */
+	static boolean isXsuaaToken(DecodedJwt decodedJwt) {
+		String jwtPayload = decodedJwt.getPayload().toLowerCase();
+		return jwtPayload.contains(EXTERNAL_ATTRIBUTE)
+				&& jwtPayload.contains(EXTERNAL_ATTRIBUTE_ENHANCER)
+				&& jwtPayload.contains("xsuaa");
+	}
+
+	/**
+	 * Checks value of environment variable 'IAS_XSUAA_XCHANGE_ENABLED'. This value
+	 * determines, whether token exchange between IAS and XSUAA is enabled. If
+	 * IAS_XSUAA_XCHANGE_ENABLED is not provided or with an empty value or with
+	 * value = false, then token exchange is disabled. Any other values are
+	 * interpreted as true.
+	 *
+	 * @return returns true if exchange is enabled and false if disabled
+	 */
+	static boolean isIasToXsuaaXchangeEnabled() {
+		String isEnabled = System.getenv(IAS_XSUAA_ENABLED);
+		LOGGER.debug("System environment variable {} is set to {}", IAS_XSUAA_ENABLED, isEnabled);
+		if (isEnabled != null) {
+			return !isEnabled.equalsIgnoreCase("false");
+		}
+		return false;
+	}
+}
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/XsuaaToken.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/XsuaaToken.java
index f93c30e6..41c4be87 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/XsuaaToken.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/XsuaaToken.java
@@ -1,6 +1,5 @@
 package com.sap.cloud.security.xsuaa.token;
 
-import com.sap.cloud.security.token.InvalidTokenException;
 import net.minidev.json.JSONArray;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -8,6 +7,7 @@ import org.springframework.lang.Nullable;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
+import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
 import org.springframework.util.Assert;
 
 import java.time.Instant;
@@ -158,7 +158,8 @@ public class XsuaaToken extends Jwt implements Token {
 				return getClaimAsString(CLIENT_ID);
 			}
 			logger.error("Couldn't get client id. Invalid authorized party or audience claims.");
-			throw new InvalidTokenException("Couldn't get client id. Invalid authorized party or audience claims.");
+			throw new InvalidBearerTokenException(
+					"Couldn't get client id. Invalid authorized party or audience claims.");
 		} else {
 			return clientId;
 		}


Test patch:
diff --git a/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java b/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java
index dfa0f652..d33898c7 100644
--- a/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java
+++ b/java-security-test/src/main/java/com/sap/cloud/security/test/JwtGenerator.java
@@ -43,7 +43,7 @@ public class JwtGenerator {
 	private Service service;
 
 	private JwtSignatureAlgorithm signatureAlgorithm;
-	private PrivateKey privateKey;
+	private PrivateKey privateKey = RSAKeys.generate().getPrivate();
 	private String appId; // this is specific to XSUAA service
 	private List<String> scopes = new ArrayList<>();
 	private List<String> localScopes = new ArrayList<>();
diff --git a/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java b/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java
index 09926a78..4b0779d4 100644
--- a/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java
+++ b/java-security-test/src/test/java/com/sap/cloud/security/test/JwtGeneratorTest.java
@@ -52,7 +52,8 @@ public class JwtGeneratorTest {
 
 	@Before
 	public void setUp() {
-		cut = JwtGenerator.getInstance(XSUAA, DEFAULT_CLIENT_ID).withPrivateKey(keys.getPrivate());
+		cut = JwtGenerator.getInstance(XSUAA, DEFAULT_CLIENT_ID)
+				.withPrivateKey(keys.getPrivate());
 	}
 
 	@Test
@@ -100,7 +101,7 @@ public class JwtGeneratorTest {
 
 	@Test
 	public void createToken_withoutPrivateKey_throwsException() {
-		assertThatThrownBy(() -> JwtGenerator.getInstance(IAS, "T00001234").createToken())
+		assertThatThrownBy(() -> JwtGenerator.getInstance(IAS, "T00001234").withPrivateKey(null).createToken())
 				.isInstanceOf(IllegalStateException.class);
 	}
 
@@ -223,7 +224,7 @@ public class JwtGeneratorTest {
 
 	@Test
 	public void withScopes_serviceIsIAS_throwsUnsupportedOperationException() {
-		cut = JwtGenerator.getInstance(IAS, "T00001234").withPrivateKey(keys.getPrivate());
+		cut = JwtGenerator.getInstance(IAS, "T00001234");
 		assertThatThrownBy(() -> cut.withScopes("firstScope").createToken())
 				.isInstanceOf(UnsupportedOperationException.class)
 				.hasMessage("Scopes are not supported for service IAS");
@@ -327,7 +328,6 @@ public class JwtGeneratorTest {
 	@Test
 	public void getInstanceFromFile_overridesTokenPropertiesForTesting() throws IOException {
 		Token token = JwtGenerator.getInstanceFromFile(XSUAA, "/token.json")
-				.withPrivateKey(keys.getPrivate())
 				.createToken();
 
 		assertThat(token.getHeaderParameterAsString(TokenHeader.KEY_ID)).isEqualTo(DEFAULT_KEY_ID);
@@ -337,7 +337,6 @@ public class JwtGeneratorTest {
 	@Test
 	public void getInstanceFromFile_loadsJsonData() throws IOException {
 		Token token = JwtGenerator.getInstanceFromFile(XSUAA, "/token.json")
-				.withPrivateKey(keys.getPrivate())
 				.createToken();
 
 		assertThat(token.getClaimAsString(TokenClaims.XSUAA.ZONE_ID)).isEqualTo("zone-id");
@@ -351,7 +350,6 @@ public class JwtGeneratorTest {
 	@Test
 	public void getInstanceFromFile_noHeader_noErrorAndReadsPayload() throws IOException {
 		Token token = JwtGenerator.getInstanceFromFile(XSUAA, "/token_no_header.json")
-				.withPrivateKey(keys.getPrivate())
 				.createToken();
 
 		assertThat(token.getClaimAsString(TokenClaims.XSUAA.ZONE_ID)).isEqualTo("zone-id");
@@ -380,8 +378,7 @@ public class JwtGeneratorTest {
 		JwtGenerator instance = JwtGenerator.getInstance(XSUAA, (key, alg, data) -> {
 			throw new NoSuchAlgorithmException();
 		});
-		cut = instance.withPrivateKey(keys.getPrivate());
-		assertThatThrownBy(() -> cut.createToken()).isInstanceOf(RuntimeException.class);
+		assertThatThrownBy(() -> instance.createToken()).isInstanceOf(RuntimeException.class);
 	}
 
 	@Test
@@ -389,8 +386,7 @@ public class JwtGeneratorTest {
 		JwtGenerator instance = JwtGenerator.getInstance(XSUAA, (key, alg, data) -> {
 			throw new SignatureException();
 		});
-		cut = instance.withPrivateKey(keys.getPrivate());
-		assertThatThrownBy(() -> cut.createToken()).isInstanceOf(RuntimeException.class);
+		assertThatThrownBy(() -> instance.createToken()).isInstanceOf(RuntimeException.class);
 	}
 
 	@Test
@@ -398,8 +394,7 @@ public class JwtGeneratorTest {
 		JwtGenerator instance = JwtGenerator.getInstance(XSUAA, (key, alg, data) -> {
 			throw new InvalidKeyException();
 		});
-		cut = instance.withPrivateKey(keys.getPrivate());
-		assertThatThrownBy(() -> cut.createToken()).isInstanceOf(RuntimeException.class);
+		assertThatThrownBy(() -> instance.createToken()).isInstanceOf(RuntimeException.class);
 	}
 
 }
diff --git a/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/IasXsuaaExchangeBrokerTest.java b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/IasXsuaaExchangeBrokerTest.java
new file mode 100644
index 00000000..b986d896
--- /dev/null
+++ b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/IasXsuaaExchangeBrokerTest.java
@@ -0,0 +1,72 @@
+package com.sap.cloud.security.xsuaa.extractor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.client.OAuth2TokenService;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = { TokenBrokerTestConfiguration.class })
+public class IasXsuaaExchangeBrokerTest {
+
+	@Autowired
+	private OAuth2TokenService oAuth2TokenService;
+
+	private MockHttpServletRequest request;
+	private static IasXsuaaExchangeBroker cut;
+	private static final String XSUAA_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHRfYXR0ciI6eyJlbmhhbmNlciI6IlhTVUFBIn19._cocFCqqATDXx6eBUoF22W9F8VwUVYY59XdLGdEDFso";
+	private static final String IAS_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJncmFudF90eXBlIjoiY2xpZW50X2NyZWRlbnRpYWxzIiwidXNlcl9pZCI6IjEwMDIxOTEiLCJ1c2VyX25hbWUiOiJXT0xGR0FORyIsImVtYWlsIjoiV09MRkdBTkdAdW5rbm93biIsImlhdCI6MTQ0MjkxMjI0NCwiZXhwIjoxNDQyOTU1MzIyLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiZXh0X2F0dHIiOnsic2VydmljZWluc3RhbmNlaWQiOiJhYmNkMTIzNCJ9LCJ6aWQiOiJ6b25lSWQifQ.uzdP2WyfullaZ_5aSjMr2shTfyqqXxbfgShjtfPjigY";
+
+	@Before
+	public void setUp() {
+		request = new MockHttpServletRequest();
+		cut = new IasXsuaaExchangeBroker(getXsuaaServiceConfiguration(), oAuth2TokenService);
+	}
+
+	@Test
+	public void xsuaaTokenResolutionTest() {
+		request.addHeader("Authorization", "bearer " + XSUAA_TOKEN);
+		String token = cut.resolve(request);
+
+		assertThat(token).isEqualTo(XSUAA_TOKEN);
+	}
+
+	@Test
+	public void iasTokenResolutionTest() {
+		request.addHeader("Authorization", "bearer " + IAS_TOKEN);
+		String token = cut.resolve(request);
+
+		assertThat(token).isEqualTo(XSUAA_TOKEN);
+	}
+
+	@Test(expected = InvalidBearerTokenException.class)
+	public void invalidAuthorizationHeaderTest() {
+		request.addHeader("Authorization", IAS_TOKEN);
+		cut.resolve(request);
+	}
+
+	@Test(expected = InvalidBearerTokenException.class)
+	public void invalidAuthorizationHeader2Test() {
+		request.addHeader("Auth", "bearer " + IAS_TOKEN);
+		cut.resolve(request);
+	}
+
+	private XsuaaServiceConfiguration getXsuaaServiceConfiguration() {
+		XsuaaServiceConfigurationDummy cfg = new XsuaaServiceConfigurationDummy();
+		cfg.appId = "a1!123";
+		cfg.clientId = "myclient!t1";
+		cfg.clientSecret = "top.secret";
+		cfg.uaaDomain = "auth.com";
+		cfg.uaaUrl = "https://mydomain.auth.com:8443";
+		return cfg;
+	}
+}
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenUtilTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenUtilTest.java
new file mode 100644
index 00000000..1e19b81c
--- /dev/null
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/extractor/TokenUtilTest.java
@@ -0,0 +1,39 @@
+package com.sap.cloud.security.xsuaa.extractor;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TokenUtilTest {
+
+	private static String encodedIasToken;
+	private static String encodedXsuaaToken;
+
+	@Before
+	public void setup() throws IOException {
+		encodedIasToken = IOUtils.resourceToString("/token_cc.txt", StandardCharsets.UTF_8);
+		encodedXsuaaToken = IOUtils.resourceToString("/token_xsuaa.txt", StandardCharsets.UTF_8);
+	}
+
+	@Test
+	public void isXsuaaTokenFalseTest() {
+		assertFalse(TokenUtil.isXsuaaToken(TokenUtil.decodeJwt(encodedIasToken)));
+	}
+
+	@Test
+	public void isXsuaaTokenTrueTest() {
+		assertTrue(TokenUtil.isXsuaaToken(TokenUtil.decodeJwt(encodedXsuaaToken)));
+	}
+
+	@Test
+	public void isXchangeEnabledTest() {
+		assertFalse(TokenUtil.isIasToXsuaaXchangeEnabled());
+	}
+
+}
\ No newline at end of file
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/IasXsuaaExchangeBrokerTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/IasXsuaaExchangeBrokerTest.java
deleted file mode 100644
index 3e600b25..00000000
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/IasXsuaaExchangeBrokerTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.sap.cloud.security.xsuaa.token;
-
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfigurationDefault;
-import com.sap.cloud.security.xsuaa.extractor.IasXsuaaExchangeBroker;
-import org.apache.commons.io.IOUtils;
-import org.junit.Test;
-import org.junit.Before;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-@ContextConfiguration(classes = XsuaaServiceConfigurationDefault.class)
-public class IasXsuaaExchangeBrokerTest {
-
-	@Autowired
-	XsuaaServiceConfiguration serviceConfiguration;
-
-	private static String encodedIasToken;
-	private static String encodedXsuaaToken;
-	private static IasXsuaaExchangeBroker tokenWrapper;
-
-	@Before
-	public void setup() throws IOException {
-		encodedIasToken = IOUtils.resourceToString("/token_cc.txt", StandardCharsets.UTF_8);
-		encodedXsuaaToken = IOUtils.resourceToString("/token_xsuaa.txt", StandardCharsets.UTF_8);
-		tokenWrapper = mock(IasXsuaaExchangeBroker.class);
-	}
-
-	@Test
-	public void isXsuaaTokenFalseTest() {
-		when(tokenWrapper.isXsuaaToken(encodedIasToken)).thenCallRealMethod();
-		assertFalse(tokenWrapper.isXsuaaToken(encodedIasToken));
-	}
-
-	@Test
-	public void isXsuaaTokenTrueTest() {
-		when(tokenWrapper.isXsuaaToken(encodedXsuaaToken)).thenCallRealMethod();
-		assertTrue(tokenWrapper.isXsuaaToken(encodedXsuaaToken));
-	}
-
-}
\ No newline at end of file