Repository: SAP/cloud-security-xsuaa-integration
Pull request #82 (2019-05-10 13:26:47)

Update README.md
fixed typo server -> serves


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59e2cbc5..6294ea46 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file.
 ## 1.4.0
 * API method to query [token validity](https://github.com/SAP/cloud-security-xsuaa-integration/blob/master/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java#L167)
 * Bugfix in basic authentication support: allow  usage of JWT token or basic authentication with one configuration
+* Allows overwrite / enhancement of XSUAA jwt token validators
 * Allow applications to initialize of Spring SecurityContext for non HTTP requests. As documented [here](https://github.com/SAP/cloud-security-xsuaa-integration/blob/master/spring-xsuaa/README.md)
 
 ## 1.3.1
diff --git a/samples/spring-security-xsuaa-usage/README.md b/samples/spring-security-xsuaa-usage/README.md
index 34c84cd5..a83009e6 100644
--- a/samples/spring-security-xsuaa-usage/README.md
+++ b/samples/spring-security-xsuaa-usage/README.md
@@ -1,6 +1,6 @@
 # Description
 This sample uses the SAP application router as a web server and forwards requests to a Java Spring back-end application running on the CF community Java buildpack.
-In a typcal UI5 application, the application router server HTML files and REST data would be provided by a back-end application. To focus on the security part, UI5 has been omitted.
+In a typcal UI5 application, the application router serves HTML files and REST data would be provided by a back-end application. To focus on the security part, UI5 has been omitted.
 
 # Coding
 This sample is using the spring-security project. As of version 5 of spring-security, this includes the OAuth resource-server functionality. The security configuration needs to configure JWT for authentication.
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java
index 3a1fa3d0..68894e86 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java
@@ -13,6 +13,7 @@ public interface Token extends UserDetails {
 	String CLAIM_XS_USER_ATTRIBUTES = "xs.user.attributes";
 	String CLAIM_SCOPES = "scope";
 	String GRANTTYPE_CLIENTCREDENTIAL = "client_credentials";
+	String CLIENT_ID = "cid";
 
 	/**
 	 * Subaccount identifier, which can be used as tenant guid
@@ -141,8 +142,7 @@ public interface Token extends UserDetails {
 	String requestToken(XSTokenRequest tokenRequest) throws URISyntaxException;
 
 	/**
-	 * Returns list of scopes with appId prefix, e.g.
-	 * "&lt;my-app!t123&gt;.Display".
+	 * Returns list of scopes with appId prefix, e.g. "&lt;my-app!t123&gt;.Display".
 	 *
 	 * @return all scopes
 	 */
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java
index b05c4ed9..b1000109 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java
@@ -67,7 +67,8 @@ public class TokenImpl implements Token {
 		return this.authorities;
 	}
 
-	@Override public Date getExpirationDate() {
+	@Override
+	public Date getExpirationDate() {
 		return jwt.getExpiresAt() != null ? Date.from(jwt.getExpiresAt()) : null;
 	}
 
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java
index e347a4a7..c95c24ca 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java
@@ -1,45 +1,74 @@
 package com.sap.cloud.security.xsuaa.token.authentication;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.XsuaaServicesParser;
 import com.sap.cloud.security.xsuaa.token.Token;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.util.Assert;
 
 /**
  * Validate audience using audience field content. in case this field is empty,
  * the audience is derived from the scope field
- *
  */
 public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
-	private XsuaaServiceConfiguration xsuaaServiceConfiguration;
+	private Map<String, String> appIdClientIdMap = new HashMap<>();
+	private final Log logger = LogFactory.getLog(XsuaaServicesParser.class);
 
 	public XsuaaAudienceValidator(XsuaaServiceConfiguration xsuaaServiceConfiguration) {
-		this.xsuaaServiceConfiguration = xsuaaServiceConfiguration;
+		Assert.notNull(xsuaaServiceConfiguration, "'xsuaaServiceConfiguration' is required");
+		appIdClientIdMap.put(xsuaaServiceConfiguration.getAppId(), xsuaaServiceConfiguration.getClientId());
+	}
+
+	public void configureAnotherXsuaaInstance(String appId, String clientId) {
+		Assert.notNull(appId, "'appId' is required");
+		Assert.notNull(clientId, "'clientId' is required");
+		appIdClientIdMap.putIfAbsent(appId, clientId);
+		logger.info(String.format("configured XsuaaAudienceValidator with appId %s and clientId %s", appId, clientId));
 	}
 
 	@Override
 	public OAuth2TokenValidatorResult validate(Jwt token) {
+		String tokenClientId = token.getClaimAsString(Token.CLIENT_ID);
+		if (tokenClientId == null) {
+			OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+					"Jwt token must contain 'cid' (client_id)", null));
+		}
+		List<String> allowedAudiences = getAllowedAudiences(token);
+
+		for (Map.Entry<String, String> xsuaaConfig : appIdClientIdMap.entrySet()) {
+			if (checkMatch(xsuaaConfig.getKey(), xsuaaConfig.getValue(), tokenClientId, allowedAudiences)) {
+				return OAuth2TokenValidatorResult.success();
+			}
+		}
+		return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
+				"Jwt token audience matches none of these: " + appIdClientIdMap.keySet().toString(), null));
+	}
+
+	private boolean checkMatch(String appId, String clientId, String tokenClientId, List<String> allowedAudiences) {
 		// case 1 : token issued by own client (or master)
-		if (xsuaaServiceConfiguration.getClientId().equals(token.getClaimAsString("client_id"))
-				|| (xsuaaServiceConfiguration.getAppId().contains("!b")
-						&& token.getClaimAsString("client_id").contains("|")
-						&& token.getClaimAsString("client_id").endsWith("|" + xsuaaServiceConfiguration.getAppId()))) {
-			return OAuth2TokenValidatorResult.success();
+		if (clientId.equals(tokenClientId)
+				|| (appId.contains("!b")
+						&& tokenClientId.contains("|")
+						&& tokenClientId.endsWith("|" + appId))) {
+			return true;
 		} else {
 			// case 2: foreign token
-			List<String> allowedAudiences = getAllowedAudiences(token);
-			if (allowedAudiences.contains(xsuaaServiceConfiguration.getAppId())) {
-				return OAuth2TokenValidatorResult.success();
+			if (allowedAudiences.contains(appId)) {
+				return true;
 			} else {
-				return OAuth2TokenValidatorResult.failure(new OAuth2Error(OAuth2ErrorCodes.INVALID_CLIENT,
-						"Missing audience " + xsuaaServiceConfiguration.getAppId(), null));
+				return false;
 			}
 		}
 	}
@@ -51,7 +80,7 @@ public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
 	 * @param token
 	 * @return (empty) list of audiences
 	 */
-	List<String> getAllowedAudiences(Jwt token) {
+	static List<String> getAllowedAudiences(Jwt token) {
 		List<String> allAudiences = new ArrayList<>();
 		List<String> tokenAudiences = token.getAudience();
 
@@ -78,7 +107,7 @@ public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
 		return allAudiences.stream().distinct().filter(value -> !value.isEmpty()).collect(Collectors.toList());
 	}
 
-	private List<String> getScopes(Jwt token) {
+	static List<String> getScopes(Jwt token) {
 		List<String> scopes = null;
 		scopes = token.getClaimAsStringList(Token.CLAIM_SCOPES);
 		return scopes != null ? scopes : new ArrayList<>();
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
index 45ff6fdc..f255f4f5 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
@@ -1,6 +1,9 @@
 package com.sap.cloud.security.xsuaa.token.authentication;
 
 import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.TimeUnit;
 
 import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
@@ -24,10 +27,21 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 
 	Cache<String, JwtDecoder> cache;
 	private XsuaaServiceConfiguration xsuaaServiceConfiguration;
+	private List<OAuth2TokenValidator<Jwt>> tokenValidators = new ArrayList<>();
 
-	XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidity, int cacheSize) {
-		cache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.SECONDS).maximumSize(cacheSize).build();
+	XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidityInSeconds, int cacheSize,
+			OAuth2TokenValidator<Jwt>... tokenValidators) {
+		cache = Caffeine.newBuilder().expireAfterWrite(cacheValidityInSeconds, TimeUnit.SECONDS).maximumSize(cacheSize)
+				.build();
 		this.xsuaaServiceConfiguration = xsuaaServiceConfiguration;
+		// configure token validators
+		this.tokenValidators.add(new JwtTimestampValidator());
+
+		if (tokenValidators == null) {
+			this.tokenValidators.add(new XsuaaAudienceValidator(xsuaaServiceConfiguration));
+		} else {
+			this.tokenValidators.addAll(Arrays.asList(tokenValidators));
+		}
 	}
 
 	@Override
@@ -45,12 +59,10 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 		}
 	}
 
-	private JwtDecoder getDecoder(String zid, String subdomain) {
+	protected JwtDecoder getDecoder(String zid, String subdomain) {
 		String url = xsuaaServiceConfiguration.getTokenKeyUrl(zid, subdomain);
 		NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(url);
-		OAuth2TokenValidator<Jwt> validators = new DelegatingOAuth2TokenValidator<>(new JwtTimestampValidator(),
-				new XsuaaAudienceValidator(xsuaaServiceConfiguration));
-		decoder.setJwtValidator(validators);
+		decoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(tokenValidators));
 		return decoder;
 	}
 
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
index fe1c3dd7..f4482925 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
@@ -1,18 +1,20 @@
 package com.sap.cloud.security.xsuaa.token.authentication;
 
-import org.springframework.security.oauth2.jwt.JwtDecoder;
-
 import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
 
 public class XsuaaJwtDecoderBuilder {
 
 	private XsuaaServiceConfiguration configuration;
-	int decoderCacheValidity = 900;
+	int decoderCacheValidity = 900; // in seconds
 	int decoderCacheSize = 100;
+	OAuth2TokenValidator<Jwt>[] tokenValidators;
 
 	/**
 	 * Utility for building a JWT decoder configuration
-	 * 
+	 *
 	 * @param configuration
 	 *            of the Xsuaa service
 	 */
@@ -22,11 +24,11 @@ public class XsuaaJwtDecoderBuilder {
 
 	/**
 	 * Assembles a JwtDecoder
-	 * 
+	 *
 	 * @return JwtDecoder
 	 */
 	public JwtDecoder build() {
-		return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize);
+		return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize, tokenValidators);
 	}
 
 	/**
@@ -55,4 +57,15 @@ public class XsuaaJwtDecoderBuilder {
 		return this;
 	}
 
+	/**
+	 * Configures clone token validator, in case of two xsuaa bindings (application
+	 * and broker plan).
+	 *
+	 * @return this
+	 */
+	public XsuaaJwtDecoderBuilder withTokenValidators(OAuth2TokenValidator<Jwt>... tokenValidators) {
+		this.tokenValidators = tokenValidators;
+		return this;
+	}
+
 }
diff --git a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java
index 85f525c0..e3f90a38 100644
--- a/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java
+++ b/spring-xsuaa/src/main/java/com/sap/xs2/security/container/SecurityContext.java
@@ -37,12 +37,13 @@ public class SecurityContext {
 	 * Obtain the Token object from the Spring SecurityContext
 	 *
 	 * @return Token object
-	 * @throws AccessDeniedException  in case there is no token, user is not authenticated
+	 * @throws AccessDeniedException
+	 *             in case there is no token, user is not authenticated
 	 */
 	static public Token getToken() {
 		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
 
-		if(authentication == null) {
+		if (authentication == null) {
 			throw new AccessDeniedException("Access forbidden: not authenticated");
 		}
 
@@ -53,7 +54,7 @@ public class SecurityContext {
 		return (Token) principal;
 	}
 
-	static public void init(String appId , Jwt token, boolean extractLocalScopesOnly) {
+	static public void init(String appId, Jwt token, boolean extractLocalScopesOnly) {
 		TokenAuthenticationConverter authenticationConverter = new TokenAuthenticationConverter(appId);
 		authenticationConverter.setLocalScopeAsAuthorities(extractLocalScopesOnly);
 		Authentication authentication = authenticationConverter.convert(token);


Test patch:
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenAuthenticationConverterTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenAuthenticationConverterTest.java
index e6098e64..a4570c6d 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenAuthenticationConverterTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenAuthenticationConverterTest.java
@@ -81,7 +81,7 @@ public class TokenAuthenticationConverterTest {
 	@Test
 	public void authoritiesHaveLocalScopesWithoutAppIdPrefix() {
 		String scopeWithNamespace = xsAppName + ".iot.Delete";
-		String scopeWithOtherAppId = "anyAppId!200." + xsAppName + ".iot.Delete";
+		String scopeWithOtherAppId = "anyAppId!t200." + xsAppName + ".Delete";
 
 		Jwt jwt = new JwtGenerator()
 				.addScopes(xsAppName + "." + scopeAdmin, scopeRead, scopeWithNamespace, scopeWithOtherAppId)
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorForCloneTokenTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorForCloneTokenTest.java
new file mode 100644
index 00000000..0f0146f4
--- /dev/null
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorForCloneTokenTest.java
@@ -0,0 +1,66 @@
+package com.sap.cloud.security.xsuaa.token.authentication;
+
+import static org.hamcrest.CoreMatchers.is;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.test.JwtGenerator;
+import com.sap.cloud.security.xsuaa.token.Token;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
+import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
+
+public class XsuaaAudienceValidatorForCloneTokenTest {
+
+	private JWTClaimsSet.Builder claimsBuilder;
+	private String XSUAA_BROKER_XSAPPNAME = "brokerplanmasterapp!b123";
+	private String XSUAA_BROKER_CLIENT_ID = "sb-" + XSUAA_BROKER_XSAPPNAME;
+	private XsuaaAudienceValidator cut;
+
+	@Before
+	public void setup() {
+		XsuaaServiceConfiguration serviceConfiguration = new XsuaaAudienceValidatorTest.DummyXsuaaServiceConfiguration(
+				"sb-test1!t1", "test1!t1");
+		cut = new XsuaaAudienceValidator(serviceConfiguration);
+		cut.configureAnotherXsuaaInstance(XSUAA_BROKER_XSAPPNAME, XSUAA_BROKER_CLIENT_ID);
+
+		claimsBuilder = new JWTClaimsSet.Builder().issueTime(new Date()).expirationTime(JwtGenerator.NO_EXPIRE_DATE);
+	}
+
+	@Test
+	public void tokenWithClientId_like_brokerClientId_shouldBeIgnored() {
+		claimsBuilder.claim(Token.CLIENT_ID, XSUAA_BROKER_CLIENT_ID);
+
+		OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
+		Assert.assertFalse(result.hasErrors());
+	}
+
+	@Test
+	public void cloneTokenClientId_like_brokerClientId_shouldBeAccepted() {
+		claimsBuilder.claim(Token.CLIENT_ID, "sb-clone1!b22|" + XSUAA_BROKER_XSAPPNAME);
+
+		OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
+		Assert.assertFalse(result.hasErrors());
+	}
+
+	@Test
+	public void cloneTokenClientId_unlike_brokerClientId_raisesError() {
+		claimsBuilder.claim(Token.CLIENT_ID, "sb-clone1!b22|ANOTHERAPP!b12");
+
+		OAuth2TokenValidatorResult result = cut.validate(JwtGenerator.createFromClaims(claimsBuilder.build()));
+		Assert.assertTrue(result.hasErrors());
+
+		List<OAuth2Error> errors = new ArrayList<>(result.getErrors());
+		Assert.assertThat(errors.get(0).getDescription(),
+				is("Jwt token audience matches none of these: [test1!t1, brokerplanmasterapp!b123]"));
+		Assert.assertThat(errors.get(0).getErrorCode(), is(OAuth2ErrorCodes.INVALID_CLIENT));
+	}
+
+}
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java
index 09e7384c..0a031017 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java
@@ -11,6 +11,8 @@ import java.util.List;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.security.oauth2.jwt.Jwt;
 
@@ -23,7 +25,6 @@ public class XsuaaAudienceValidatorTest {
 
 	private Jwt tokenWithAudience;
 	private Jwt tokenWithoutAudience;
-	private Jwt cloneTokenWithoutAudience;
 	private Jwt cloneTokenWithAudience;
 
 	private XsuaaServiceConfiguration serviceConfigurationSameClientId;
@@ -42,9 +43,9 @@ public class XsuaaAudienceValidatorTest {
 		tokenWithAudience = new JwtGenerator().createFromTemplate("/audience_1.txt");
 		tokenWithoutAudience = new JwtGenerator().createFromTemplate("/audience_2.txt");
 		cloneTokenWithAudience = new JwtGenerator().createFromTemplate("/audience_3.txt");
-		cloneTokenWithoutAudience = new JwtGenerator().createFromTemplate("/audience_4.txt");
 
 		claimsBuilder = new JWTClaimsSet.Builder().issueTime(new Date()).expirationTime(JwtGenerator.NO_EXPIRE_DATE);
+		claimsBuilder.claim(Token.CLIENT_ID, "sb-test1!t1");
 	}
 
 	@Test
@@ -63,8 +64,10 @@ public class XsuaaAudienceValidatorTest {
 
 	@Test
 	public void testExtractAudiencesFromTokenScopes() {
-		Jwt token = new JwtGenerator().addScopes("test1!t1.read","test2!t1.read","test2!t1.write", ".scopeWithoutAppId").getToken();
-		List<String> audiences = new XsuaaAudienceValidator(serviceConfigurationSameClientId).getAllowedAudiences(token);
+		Jwt token = new JwtGenerator()
+				.addScopes("test1!t1.read", "test2!t1.read", "test2!t1.write", ".scopeWithoutAppId").getToken();
+		List<String> audiences = new XsuaaAudienceValidator(serviceConfigurationSameClientId)
+				.getAllowedAudiences(token);
 		Assert.assertThat(audiences.size(), is(2));
 		Assert.assertThat(audiences, hasItem("test1!t1"));
 		Assert.assertThat(audiences, hasItem("test2!t1"));
@@ -120,6 +123,9 @@ public class XsuaaAudienceValidatorTest {
 		OAuth2TokenValidatorResult result = new XsuaaAudienceValidator(serviceConfigurationOtherGrantedClientId)
 				.validate(tokenWithoutAudienceButScopes);
 		Assert.assertTrue(result.hasErrors());
+		List<OAuth2Error> errors = new ArrayList<>(result.getErrors());
+		Assert.assertThat(errors.get(0).getDescription(), is("Jwt token audience matches none of these: [test2!t1]"));
+		Assert.assertThat(errors.get(0).getErrorCode(), is(OAuth2ErrorCodes.INVALID_CLIENT));
 	}
 
 	@Test
@@ -153,7 +159,7 @@ public class XsuaaAudienceValidatorTest {
 		Assert.assertFalse(result.hasErrors());
 	}
 
-	class DummyXsuaaServiceConfiguration implements XsuaaServiceConfiguration {
+	public static class DummyXsuaaServiceConfiguration implements XsuaaServiceConfiguration {
 
 		String clientId;
 		String xsAppId;
diff --git a/spring-xsuaa/src/test/resources/audience_1.txt b/spring-xsuaa/src/test/resources/audience_1.txt
index a1b1df5b..2c0f2da8 100644
--- a/spring-xsuaa/src/test/resources/audience_1.txt
+++ b/spring-xsuaa/src/test/resources/audience_1.txt
@@ -1,5 +1,5 @@
 {
- "client_id":"sb-test1!t1",
+ "cid":"sb-test1!t1",
  "exp":$exp,
  "scope":["test1!t1.read","test2!t1.write"],
  "user_name":"testUser",
diff --git a/spring-xsuaa/src/test/resources/audience_2.txt b/spring-xsuaa/src/test/resources/audience_2.txt
index e49b2a80..4815c43e 100644
--- a/spring-xsuaa/src/test/resources/audience_2.txt
+++ b/spring-xsuaa/src/test/resources/audience_2.txt
@@ -1,5 +1,5 @@
 {
- "client_id":"sb-test1!t1",
+ "cid":"sb-test1!t1",
  "exp":$exp,
  "scope":["test1!t1.read","test2!t1.write"],
  "user_name":"testUser",
diff --git a/spring-xsuaa/src/test/resources/audience_3.txt b/spring-xsuaa/src/test/resources/audience_3.txt
index 37336b70..2c68e5a4 100644
--- a/spring-xsuaa/src/test/resources/audience_3.txt
+++ b/spring-xsuaa/src/test/resources/audience_3.txt
@@ -1,5 +1,5 @@
 {
- "client_id":"sb-clone1!b22|test3!b1",
+ "cid":"sb-clone1!b22|test3!b1",
  "exp":$exp,
  "scope":["test1!t1.read","test2!t1.write"],
  "user_name":"testUser",
@@ -7,6 +7,6 @@
  "email":"testUser@testOrg",
  "zid":"demo",
  "grant_type":"password",
- "aud":["b22|test3!b1.read"],
+ "aud":["clone1!b22|test3!b1"],
  "iat": 1532416849
 }
\ No newline at end of file
diff --git a/spring-xsuaa/src/test/resources/audience_4.txt b/spring-xsuaa/src/test/resources/audience_4.txt
index 221954ea..a0495a3f 100644
--- a/spring-xsuaa/src/test/resources/audience_4.txt
+++ b/spring-xsuaa/src/test/resources/audience_4.txt
@@ -1,5 +1,5 @@
 {
- "client_id":"sb-clone1!b22|test3!b1",
+ "cid":"sb-clone1!b22|test3!b1",
  "exp":$exp,
  "scope":["test1!t1.read","test2!t1.write"],
  "user_name":"testUser",