Repository: SAP/cloud-security-xsuaa-integration
Pull request #432 (2020-12-14 18:51:51)

fix compatibility with nimbus-jose-jwt version < 9.0 #414

nenaraab: Hi @liga-oz

can you please analyze the IT tests:
https://github.com/SAP/cloud-security-xsuaa-integration/pull/432/checks?check_run_id=1552396992

Caused by: java.lang.ClassNotFoundException: com.nimbusds.jose.shaded.json.parser.ParseException
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382) ~[na:1.8.0_275]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:419) ~[na:1.8.0_275]
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352) ~[na:1.8.0_275]
	at java.lang.ClassLoader.loadClass(ClassLoader.java:352) ~[na:1.8.0_275]
	... 73 common frames omitted

Do you see a way to change it without incompatibilities to our consumers? I think we can not force them to upgrade to the latest Spring versions...

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b5c90897..1fbc0acf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,11 @@
 # Change Log 
 
 All notable changes to this project will be documented in this file.
-
+## 2.8.1
+- The following dependencies were updated:
+    - spring.boot.version 2.3.5.RELEASE --> 2.4.1
+    - spring.core.version 5.2.10.RELEASE --> 5.3.2
+    - spring.security.version 5.3.5.RELEASE --> 5.4.2
 ## 2.8.0
 - [java-security] 
   - `getClientId()` method was added to `Token` interface. `getClientId()` method should be used instead of `getClaimAsString(TokenClaims.XSUAA.CLIENT_ID)`. `TokenClaims.XSUAA.CLIENT_ID` is deprecated.
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 0251223d..98dfe19e 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
@@ -5,6 +5,7 @@ import com.sap.cloud.security.json.JsonObject;
 import com.sap.cloud.security.xsuaa.Assertions;
 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;
 
@@ -14,10 +15,7 @@ import java.security.Principal;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Set;
+import java.util.*;
 import java.util.regex.Pattern;
 
 import static com.sap.cloud.security.token.TokenClaims.*;
@@ -122,7 +120,7 @@ public abstract class AbstractToken implements Token {
 	public boolean isXsuaaToken() {
 		if (tokenBody.contains(EXTERNAL_ATTRIBUTE)) {
 			JsonObject externalAttributes = tokenBody.getJsonObject(EXTERNAL_ATTRIBUTE);
-			if ("XSUAA".equals(externalAttributes.getAsString(EXTERNAL_ATTRIBUTE_ENHANCER))) {
+			if ("XSUAA".equalsIgnoreCase(externalAttributes.getAsString(EXTERNAL_ATTRIBUTE_ENHANCER))) {
 				return true;
 			}
 		}
@@ -205,4 +203,14 @@ public abstract class AbstractToken implements Token {
 	public String toString() {
 		return decodedJwt.toString();
 	}
+
+	@Override
+	public Map<String, Object> getHeaders() {
+		return new JSONObject(decodedJwt.getHeader()).toMap();
+	}
+
+	@Override
+	public Map<String, Object> getClaims() {
+		return new JSONObject(decodedJwt.getPayload()).toMap();
+	}
 }
diff --git a/pom.xml b/pom.xml
index c5ebf3ee..981389ad 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,9 +56,9 @@
 		<maven.compiler.source>1.8</maven.compiler.source>
 		<maven.source.plugin.version>3.1.0</maven.source.plugin.version>
 		<!-- make sure that spring core and spring boot versions are compatible-->
-		<spring.boot.version>2.3.5.RELEASE</spring.boot.version>
-		<spring.core.version>5.2.10.RELEASE</spring.core.version>
-		<spring.security.version>5.3.5.RELEASE</spring.security.version>
+		<spring.boot.version>2.4.1</spring.boot.version>
+		<spring.core.version>5.3.2</spring.core.version>
+		<spring.security.version>5.4.2</spring.security.version>
 		<spring.security.oauth2.version>2.5.0.RELEASE</spring.security.oauth2.version>
 		<spring.security.jwt.version>1.1.1.RELEASE</spring.security.jwt.version>
 		<reactor.version>3.3.7.RELEASE</reactor.version>
@@ -464,7 +464,7 @@
 					<plugin>
 						<groupId>org.apache.maven.plugins</groupId>
 						<artifactId>maven-javadoc-plugin</artifactId>
-						<version>2.9.1</version>
+						<version>3.2.0</version>
 						<executions>
 							<execution>
 								<id>attach-javadocs</id>
diff --git a/samples/spring-webflux-security-xsuaa-usage/pom.xml b/samples/spring-webflux-security-xsuaa-usage/pom.xml
index 108da140..055068f7 100644
--- a/samples/spring-webflux-security-xsuaa-usage/pom.xml
+++ b/samples/spring-webflux-security-xsuaa-usage/pom.xml
@@ -6,7 +6,7 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.3.4.RELEASE</version>
+        <version>2.4.1</version>
         <relativePath/>
     </parent>
 
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServicesParser.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServicesParser.java
index ee83488f..8a9b5234 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServicesParser.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/XsuaaServicesParser.java
@@ -6,21 +6,16 @@ import javax.annotation.Nullable;
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Properties;
-import java.util.Set;
+import java.util.*;
 
 import org.apache.commons.io.Charsets;
 import org.apache.commons.io.IOUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import net.minidev.json.JSONArray;
-import net.minidev.json.JSONObject;
-import net.minidev.json.parser.JSONParser;
-import net.minidev.json.parser.ParseException;
-
 public class XsuaaServicesParser {
 
 	private static final Logger logger = LoggerFactory.getLogger(XsuaaServicesParser.class);
@@ -73,11 +68,12 @@ public class XsuaaServicesParser {
 			credentialsJSON = parseCredentials(vcapServices);
 		}
 		if (credentialsJSON != null) {
-			Optional<String> attributeString = Optional.ofNullable(credentialsJSON.getAsString(name));
-			if (!attributeString.isPresent()) {
+			String attributeString = credentialsJSON.optString(name);
+			if (attributeString.isEmpty()) {
 				logger.info("XSUAA VCAP_SERVICES has no attribute with name '{}'.", name);
+			} else {
+				return Optional.of(attributeString);
 			}
-			return attributeString;
 		}
 		return Optional.empty();
 	}
@@ -110,13 +106,13 @@ public class XsuaaServicesParser {
 			return null;
 		}
 		try {
-			JSONObject vcapServicesJSON = (JSONObject) new JSONParser(JSONParser.MODE_PERMISSIVE).parse(vcapServices);
+			JSONObject vcapServicesJSON = new JSONObject(vcapServices);
 			JSONObject xsuaaBinding = searchXsuaaBinding(vcapServicesJSON);
 
-			if (Objects.nonNull(xsuaaBinding) && xsuaaBinding.containsKey(CREDENTIALS)) {
+			if (Objects.nonNull(xsuaaBinding) && xsuaaBinding.has(CREDENTIALS)) {
 				return (JSONObject) xsuaaBinding.get(CREDENTIALS);
 			}
-		} catch (ParseException ex) {
+		} catch (JSONException ex) {
 			throw new IOException("Error while parsing XSUAA credentials from VCAP_SERVICES: {}.", ex);
 		}
 		return null;
@@ -135,14 +131,14 @@ public class XsuaaServicesParser {
 
 	private static JSONObject getJSONObjectFromTag(final JSONArray jsonArray, String tag) {
 		JSONObject xsuaaBinding = null;
-		for (int i = 0; i < jsonArray.size(); i++) {
-			JSONObject binding = (JSONObject) jsonArray.get(i);
+		for (Object value : jsonArray) {
+			JSONObject binding = (JSONObject) value;
 			JSONArray tags = (JSONArray) binding.get(TAGS);
 
-			Optional<String> planName = Optional.ofNullable(binding.getAsString("plan"));
-			boolean isApiAccessPlan = (planName.isPresent() && planName.get().equals("apiaccess"));
-			for (int j = 0; j < tags.size(); j++) {
-				if (tags.get(j).equals(tag) && !isApiAccessPlan) {
+			String planName = binding.optString("plan");
+			boolean isApiAccessPlan = (!planName.isEmpty() && planName.equals("apiaccess"));
+			for (Object o : tags) {
+				if (o.equals(tag) && !isApiAccessPlan) {
 					if (xsuaaBinding == null) {
 						xsuaaBinding = binding;
 					} else {
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
index f85a3325..0a3f1ee0 100644
--- 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
@@ -45,7 +45,7 @@ class TokenUtil {
 	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")),
+		return new Jwt(decodedJwt.getEncodedToken(), Instant.ofEpochSecond(payload.optLong("iat")),
 				Instant.ofEpochSecond(payload.getLong("exp")),
 				header.toMap(), payload.toMap());
 	}
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 41c4be87..3a9c704f 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,6 @@
 package com.sap.cloud.security.xsuaa.token;
 
-import net.minidev.json.JSONArray;
+import org.json.JSONArray;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.lang.Nullable;
@@ -274,10 +274,10 @@ public class XsuaaToken extends Jwt implements Token {
 		}
 
 		// convert JSONArray to String[]
-		JSONArray attributeJsonArray = (JSONArray) claimMap.get(attributeName);
+		JSONArray attributeJsonArray = new JSONArray((ArrayList)claimMap.get(attributeName));
 		if (attributeJsonArray != null) {
-			attributeValues = new String[attributeJsonArray.size()];
-			for (int i = 0; i < attributeJsonArray.size(); i++) {
+			attributeValues = new String[attributeJsonArray.length()];
+			for (int i = 0; i < attributeJsonArray.length(); i++) {
 				attributeValues[i] = (String) attributeJsonArray.get(i);
 			}
 		}
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/ReactiveXsuaaJwtDecoder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/ReactiveXsuaaJwtDecoder.java
index 28c2a3bb..46dc0bc7 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/ReactiveXsuaaJwtDecoder.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/ReactiveXsuaaJwtDecoder.java
@@ -11,6 +11,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
+import org.json.JSONObject;
 import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -44,12 +45,12 @@ public class ReactiveXsuaaJwtDecoder implements ReactiveJwtDecoder {
 		this.tokenInfoExtractor = new TokenInfoExtractor() {
 			@Override
 			public String getJku(JWT jwt) {
-				return (String) jwt.getHeader().toJSONObject().getOrDefault(CLAIM_JKU, null);
+				return new JSONObject(jwt.getHeader().toString()).optString(CLAIM_JKU, null);
 			}
 
 			@Override
 			public String getKid(JWT jwt) {
-				return (String) jwt.getHeader().toJSONObject().getOrDefault(CLAIM_KID, null);
+				return new JSONObject(jwt.getHeader().toString()).optString(CLAIM_KID, null);
 			}
 
 			@Override
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 8d3baa83..c5339c81 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
@@ -15,6 +15,7 @@ import java.text.ParseException;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
+import org.json.JSONObject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -51,12 +52,12 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 		this.tokenInfoExtractor = new TokenInfoExtractor() {
 			@Override
 			public String getJku(JWT jwt) {
-				return (String) jwt.getHeader().toJSONObject().getOrDefault(CLAIM_JKU, null);
+				return new JSONObject(jwt.getHeader().toString()).optString(CLAIM_JKU, null);
 			}
 
 			@Override
 			public String getKid(JWT jwt) {
-				return (String) jwt.getHeader().toJSONObject().getOrDefault(CLAIM_KID, null);
+				return new JSONObject(jwt.getHeader().toString()).optString(CLAIM_KID, null);
 			}
 
 			@Override


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 d33898c7..99f9a6f1 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
@@ -148,6 +148,7 @@ public class JwtGenerator {
 			withClaimValue(TokenClaims.XSUAA.CLIENT_ID, azp); // Client Id left for backward compatibility
 			jsonPayload.put(TokenClaims.AUDIENCE, Arrays.asList(azp));
 			jsonPayload.put(TokenClaims.XSUAA.ZONE_ID, DEFAULT_ZONE_ID);
+			jsonPayload.put(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE, createJsonObject("{\"enhancer\" : \"xsuaa\"} "));
 		}
 	}
 
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 4b0779d4..0c1d835f 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
@@ -3,6 +3,7 @@ package com.sap.cloud.security.test;
 import com.sap.cloud.security.json.DefaultJsonObject;
 import com.sap.cloud.security.json.JsonObject;
 import com.sap.cloud.security.json.JsonParsingException;
+import com.sap.cloud.security.token.AbstractToken;
 import com.sap.cloud.security.token.Token;
 import com.sap.cloud.security.token.TokenClaims;
 import com.sap.cloud.security.token.TokenHeader;
@@ -70,6 +71,7 @@ public class JwtGeneratorTest {
 		assertThat(token.getZoneId()).isEqualTo(DEFAULT_ZONE_ID);
 		assertThat(token.getClaimAsString(TokenClaims.XSUAA.ZONE_ID)).isEqualTo(DEFAULT_ZONE_ID);
 		assertThat(token.getExpiration()).isEqualTo(JwtGenerator.NO_EXPIRE_DATE);
+		assertThat(((AbstractToken)token).isXsuaaToken()).isTrue();
 	}
 
 	@Test
diff --git a/spring-xsuaa-test/pom.xml b/spring-xsuaa-test/pom.xml
index 1ad4376e..c4733dcf 100644
--- a/spring-xsuaa-test/pom.xml
+++ b/spring-xsuaa-test/pom.xml
@@ -43,6 +43,10 @@
 			<artifactId>hamcrest-all</artifactId>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>org.json</groupId>
+			<artifactId>json</artifactId>
+		</dependency>
 	</dependencies>
 
 	<build>
diff --git a/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java
index 7c61a544..43dcfea7 100644
--- a/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java
+++ b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java
@@ -1,20 +1,20 @@
 package com.sap.cloud.security.xsuaa.test;
 
-import java.io.IOException;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.text.ParseException;
-import java.util.*;
-
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.sap.cloud.security.xsuaa.test.jwt.Base64JwtDecoder;
+import com.sap.cloud.security.xsuaa.test.jwt.DecodedJwt;
 import org.apache.commons.io.IOUtils;
+import org.json.JSONObject;
 import org.springframework.lang.Nullable;
 import org.springframework.security.jwt.JwtHelper;
 import org.springframework.security.jwt.crypto.sign.RsaSigner;
 import org.springframework.security.oauth2.jwt.Jwt;
 
-import com.nimbusds.jwt.JWT;
-import com.nimbusds.jwt.JWTClaimsSet;
-import com.nimbusds.jwt.JWTParser;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.util.*;
 
 import static com.sap.cloud.security.xsuaa.test.JwtGenerator.TokenHeaders.JKU;
 import static com.sap.cloud.security.xsuaa.test.JwtGenerator.TokenHeaders.KID;
@@ -449,20 +449,22 @@ public class JwtGenerator {
 
 	@Nullable
 	public static Jwt convertTokenToOAuthJwt(String token) {
-		Jwt jwt;
-		try {
-			JWT parsedJwt = JWTParser.parse(token);
-			JWTClaimsSet jwtClaimsSet = parsedJwt.getJWTClaimsSet();
-			Map<String, Object> headers = new LinkedHashMap<>(parsedJwt.getHeader().toJSONObject());
-			jwt = new Jwt(parsedJwt.getParsedString(), jwtClaimsSet.getIssueTime().toInstant(),
-					jwtClaimsSet.getExpirationTime().toInstant(), headers, jwtClaimsSet.getClaims());
-		} catch (ParseException e) {
-			throw new IllegalArgumentException("token can not be parsed. ", e);
-		}
-		return jwt;
+		return parseJwt(decodeJwt(token));
+	}
+
+	private 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.optLong("iat")),
+				Instant.ofEpochSecond(payload.getLong("exp")),
+				header.toMap(), payload.toMap());
+	}
+
+	static DecodedJwt decodeJwt(String encodedJwtToken) {
+		return Base64JwtDecoder.getInstance().decode(encodedJwtToken);
 	}
 
-	class ExternalAttrClaim {
+	protected class ExternalAttrClaim {
 		public String zdn = subdomain;
 	}
 }
diff --git a/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/jwt/Base64JwtDecoder.java b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/jwt/Base64JwtDecoder.java
new file mode 100644
index 00000000..c2ee2972
--- /dev/null
+++ b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/jwt/Base64JwtDecoder.java
@@ -0,0 +1,98 @@
+package com.sap.cloud.security.xsuaa.test.jwt;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+import static java.lang.System.lineSeparator;
+
+public final class Base64JwtDecoder {
+	private static final Base64JwtDecoder instance = new Base64JwtDecoder();
+
+	/**
+	 * @deprecated in favor of the {@link #getInstance() method} and will become
+	 *             private with version 3.0.0
+	 */
+	@Deprecated
+	public Base64JwtDecoder() {
+		// becomes private with version 3.0.0
+	}
+
+	public static Base64JwtDecoder getInstance() {
+		return instance;
+	}
+
+	/**
+	 * Decodes the Json Web token (jwt).
+	 * 
+	 * @param jwt
+	 *            the access token
+	 * @return the decoded jwt.
+	 */
+	public DecodedJwt decode(String jwt) {
+		Objects.requireNonNull(jwt, "JWT must not be null");
+
+		String[] parts = jwt.split(Pattern.quote("."));
+		if (parts.length != 3) {
+			throw new IllegalArgumentException("JWT token does not consist of 'header'.'payload'.'signature'.");
+		}
+		String header = base64Decode(parts[0]);
+		String payload = base64Decode(parts[1]);
+		String signature = parts[2];
+
+		return new DecodedJwtImpl(jwt, header, payload, signature);
+	}
+
+	private String base64Decode(String encoded) {
+		byte[] decodedBytes = Base64.getUrlDecoder().decode(encoded);
+		return new String(decodedBytes, StandardCharsets.UTF_8);
+	}
+
+	static class DecodedJwtImpl implements DecodedJwt {
+
+		private String header;
+		private String payload;
+		private String signature;
+		private String encodedJwt;
+		private static final String TAB = "\t";
+
+		DecodedJwtImpl(String encodedJwt, String header, String payload, String signature) {
+			this.header = header;
+			this.payload = payload;
+			this.signature = signature;
+			this.encodedJwt = encodedJwt;
+		}
+
+		@Override
+		public String getHeader() {
+			return header;
+		}
+
+		@Override
+		public String getPayload() {
+			return payload;
+		}
+
+		@Override
+		public String getSignature() {
+			return signature;
+		}
+
+		@Override
+		public String getEncodedToken() {
+			return encodedJwt;
+		}
+
+		@Override
+		public String toString() {
+			StringBuilder stringBuilder = new StringBuilder();
+			stringBuilder.append("Jwt header" + lineSeparator());
+			stringBuilder.append(TAB + getHeader() + lineSeparator());
+			stringBuilder.append("Jwt payload" + lineSeparator());
+			stringBuilder.append(TAB + getPayload() + lineSeparator());
+
+			return stringBuilder.toString();
+		}
+	}
+}
diff --git a/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/jwt/DecodedJwt.java b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/jwt/DecodedJwt.java
new file mode 100644
index 00000000..3f0550ba
--- /dev/null
+++ b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/jwt/DecodedJwt.java
@@ -0,0 +1,44 @@
+package com.sap.cloud.security.xsuaa.test.jwt;
+
+/**
+ * A Jwt token consists of three parts, separated by ".":
+ * header.payload.signature
+ *
+ * Use {@code Base64JwtDecoder.getInstance().decode(token)} to get a
+ * {@link DecodedJwt} instance.
+ */
+
+public interface DecodedJwt {
+
+	/**
+	 * Get the base64 decoded header of the jwt as UTF-8 String.
+	 *
+	 * @return the decoded header.
+	 */
+	String getHeader();
+
+	/**
+	 * Get the base64 decoded payload of the jwt as UTF-8 String.
+	 *
+	 * @return the decoded payload.
+	 */
+	String getPayload();
+
+	/**
+	 * Get the encoded signature of the jwt.
+	 *
+	 * @return the decoded signature.
+	 */
+	String getSignature();
+
+	/**
+	 * Get the original encoded access token.
+	 *
+	 * <p>
+	 * Never expose this token via log or via HTTP.
+	 *
+	 * @return jwt token
+	 */
+	String getEncodedToken();
+
+}
diff --git a/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java b/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java
index f5861bab..be3631bb 100644
--- a/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java
+++ b/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java
@@ -5,18 +5,16 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.*;
 
 import java.io.IOException;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
 
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.sap.cloud.security.xsuaa.test.JwtGenerator.TokenClaims;
 import com.sap.cloud.security.xsuaa.test.JwtGenerator.TokenHeaders;
+import org.json.JSONArray;
 import org.junit.Before;
 import org.junit.Test;
 import org.springframework.security.oauth2.jwt.Jwt;
 
-import net.minidev.json.JSONArray;
-
 public class JwtGeneratorTest {
 	private JwtGenerator jwtGenerator;
 	private static final String MY_CLIENT_ID = "sb-clientId!20";
@@ -77,8 +75,8 @@ public class JwtGeneratorTest {
 				.addAttribute(ANOTHER_ATTRIBUTE, new String[] { ANOTHER_ATTRIBUTE_VALUE, ANOTHER_ATTRIBUTE_VALUE_2 })
 				.getToken();
 		Map<String, Object> attributes = jwt.getClaimAsMap("xs.user.attributes");
-		assertThat((JSONArray) attributes.get(DUMMY_ATTRIBUTE), contains(DUMMY_ATTRIBUTE));
-		assertThat((JSONArray) attributes.get(ANOTHER_ATTRIBUTE),
+		assertThat(new JSONArray((ArrayList) attributes.get(DUMMY_ATTRIBUTE)), contains(DUMMY_ATTRIBUTE));
+		assertThat(new JSONArray((ArrayList) attributes.get(ANOTHER_ATTRIBUTE)),
 				contains(ANOTHER_ATTRIBUTE_VALUE, ANOTHER_ATTRIBUTE_VALUE_2));
 	}
 
@@ -103,7 +101,7 @@ public class JwtGeneratorTest {
 		assertThat(jwt.getClaimAsStringList("scope"), hasItems("openid", "testScope", "testApp.localScope"));
 
 		Map<String, Object> attributes = jwt.getClaimAsMap("xs.user.attributes");
-		assertThat((JSONArray) attributes.get("usrAttr"), contains("value_1", "value_2"));
+		assertThat(new JSONArray((ArrayList) attributes.get("usrAttr")), contains("value_1", "value_2"));
 		jwt.getTokenValue();
 	}
 
diff --git a/spring-xsuaa-test/src/test/resources/claims_template.txt b/spring-xsuaa-test/src/test/resources/claims_template.txt
index fb41b917..0377c9ef 100644
--- a/spring-xsuaa-test/src/test/resources/claims_template.txt
+++ b/spring-xsuaa-test/src/test/resources/claims_template.txt
@@ -3,7 +3,7 @@
   "ext_attr": {
     "enhancer": "XSUAA",
     "serviceinstanceid": "brokerCloneServiceInstanceId",
-    "zdn": "uaa",
+    "zdn": "$zdn",
     "acl": ["app1!t23"]
   },
   "xs.system.attributes": {
@@ -33,8 +33,5 @@
   "iss": "http://paas.localhost:8080/uaa/oauth/token",
   "zid": "$zid",
   "aud": [],
-  "az_attr": {"external_id":"abcd1234"},
-  "ext_attr": {
-      "zdn": "$zdn"
-  }
+  "az_attr": {"external_id":"abcd1234"}
 }
\ No newline at end of file
diff --git a/spring-xsuaa-test/src/test/resources/token_cc.txt b/spring-xsuaa-test/src/test/resources/token_cc.txt
index 2d7b5945..d5f9d7d2 100644
--- a/spring-xsuaa-test/src/test/resources/token_cc.txt
+++ b/spring-xsuaa-test/src/test/resources/token_cc.txt
@@ -1,1 +1,1 @@
-eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJqdGkiOiAiMzc1ODdlOGM0NWI4NGE4NTgyMDc0NGMxNDU5OTEwYjUiLAogICJleHRfYXR0ciI6IHsKICAgICJlbmhhbmNlciI6ICJYU1VBQSIsCiAgICAic2VydmljZWluc3RhbmNlaWQiOiAiYnJva2VyQ2xvbmVTZXJ2aWNlSW5zdGFuY2VJZCIsCiAgICAiemRuIjogInVhYSIsCiAgICAiYWNsIjogWyJhcHAxIXQyMyJdCiAgfSwKICAieHMuc3lzdGVtLmF0dHJpYnV0ZXMiOiB7CiAgICAieHMuc2FtbC5ncm91cHMiOiBbCiAgICAgICJnMSIKICAgIF0sCiAgICAieHMucm9sZWNvbGxlY3Rpb25zIjogW10KICB9LAogICJnaXZlbl9uYW1lIjogInRlc3R1c2VyIiwKICAieHMudXNlci5hdHRyaWJ1dGVzIjogeyJ1c3JBdHRyIjogWyJ2YWx1ZV8xIiwgInZhbHVlXzIiXX0sCiAgImZhbWlseV9uYW1lIjogInVua25vd24ub3JnIiwKICAic3ViIjogImQyMWY1ZGU5LWQ3NjEtNDdhMi1iNmQ0LTJkODMxNjE1ODRkOSIsCiAgInNjb3BlIjogWwogICAgIm9wZW5pZCIsICJ0ZXN0U2NvcGUiLCAidGVzdEFwcC5sb2NhbFNjb3BlIgogIF0sCiAgImNsaWVudF9pZCI6ICJzYi1jbGllbnRJZCEyMCIsCiAgImNpZCI6ICJzYi1jbG9uZTEhYjV8TFItbWFzdGVyIWI1IiwKICAiYXpwIjogInNiLWNsb25lMSFiNXxMUi1tYXN0ZXIhYjUiLAogICJncmFudF90eXBlIjogInVybjppZXRmOnBhcmFtczpvYXV0aDpncmFudC10eXBlOnNhbWwyLWJlYXJlciIsCiAgInVzZXJfaWQiOiAiZDIxZjVkZTktZDc2MS00N2EyLWI2ZDQtMmQ4MzE2MTU4NGQ5IiwKICAib3JpZ2luIjogInVzZXJpZHAiLAogICJ1c2VyX25hbWUiOiAidGVzdHVzZXIiLAogICJlbWFpbCI6ICJ0ZXN0dXNlckB1YWEub3JnIiwKICAicmV2X3NpZyI6ICJiODUwNzU2YSIsCiAgImlhdCI6IDE1MzI0MTY4NDksCiAgImV4cCI6IDIxNDc0ODM2NDcsCiAgImlzcyI6ICJodHRwOi8vcGFhcy5sb2NhbGhvc3Q6ODA4MC91YWEvb2F1dGgvdG9rZW4iLAogICJ6aWQiOiAibXktc3ViYWNjb3VudC1zdWJkb21haW4taWQiLAogICJhdWQiOiBbXSwKICAiYXpfYXR0ciI6IHsiZXh0ZXJuYWxfaWQiOiJhYmNkMTIzNCJ9LAogICJleHRfYXR0ciI6IHsKICAgICAgInpkbiI6ICJteS1zdWJhY2NvdW50LXN1YmRvbWFpbiIKICB9Cn0.Gacy8gqDcbbpcehLEnL9V_-0Yx0JiwePLMXvQi5nrYB30aElfMSWq1YPxpri_zwk2pMk5WYFIgtLI7potsafbqVfvweypMDK25uhOkjU3Js-DW9QPz0ti0C6ZAp0Xze3HtI9nU9VqYOygmyeusCYNQVAIl5O2sAbiciWVnihPgh176ulQ9hAmyBjFWXT-zA-kVSkMHFcFyrTBfkbgYds94qXQBJQ97cmB-jg_XnJ3BVVt20cdZJg6gob9tcZaQFkpGwpx_hoHeSPzH2rpdDQkl-aozJHCg0S0PjiyUtvldghCSaYRddmDkJcU1W-omKVMzQ0mLwm0QkOv0n8jGDtoQ
\ No newline at end of file
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.ewogICJqdGkiOiAiMzc1ODdlOGM0NWI4NGE4NTgyMDc0NGMxNDU5OTEwYjUiLAogICJleHRfYXR0ciI6IHsKICAgICJlbmhhbmNlciI6ICJYU1VBQSIsCiAgICAic2VydmljZWluc3RhbmNlaWQiOiAiYnJva2VyQ2xvbmVTZXJ2aWNlSW5zdGFuY2VJZCIsCiAgICAiemRuIjogIm15LXN1YmFjY291bnQtc3ViZG9tYWluIiwKICAgICJhY2wiOiBbImFwcDEhdDIzIl0KICB9LAogICJ4cy5zeXN0ZW0uYXR0cmlidXRlcyI6IHsKICAgICJ4cy5zYW1sLmdyb3VwcyI6IFsKICAgICAgImcxIgogICAgXSwKICAgICJ4cy5yb2xlY29sbGVjdGlvbnMiOiBbXQogIH0sCiAgImdpdmVuX25hbWUiOiAidGVzdHVzZXIiLAogICJ4cy51c2VyLmF0dHJpYnV0ZXMiOiB7InVzckF0dHIiOiBbInZhbHVlXzEiLCAidmFsdWVfMiJdfSwKICAiZmFtaWx5X25hbWUiOiAidW5rbm93bi5vcmciLAogICJzdWIiOiAiZDIxZjVkZTktZDc2MS00N2EyLWI2ZDQtMmQ4MzE2MTU4NGQ5IiwKICAic2NvcGUiOiBbCiAgICAib3BlbmlkIiwgInRlc3RTY29wZSIsICJ0ZXN0QXBwLmxvY2FsU2NvcGUiCiAgXSwKICAiY2xpZW50X2lkIjogInNiLWNsaWVudElkITIwIiwKICAiY2lkIjogInNiLWNsb25lMSFiNXxMUi1tYXN0ZXIhYjUiLAogICJhenAiOiAic2ItY2xvbmUxIWI1fExSLW1hc3RlciFiNSIsCiAgImdyYW50X3R5cGUiOiAidXJuOmlldGY6cGFyYW1zOm9hdXRoOmdyYW50LXR5cGU6c2FtbDItYmVhcmVyIiwKICAidXNlcl9pZCI6ICJkMjFmNWRlOS1kNzYxLTQ3YTItYjZkNC0yZDgzMTYxNTg0ZDkiLAogICJvcmlnaW4iOiAidXNlcmlkcCIsCiAgInVzZXJfbmFtZSI6ICJ0ZXN0dXNlciIsCiAgImVtYWlsIjogInRlc3R1c2VyQHVhYS5vcmciLAogICJyZXZfc2lnIjogImI4NTA3NTZhIiwKICAiaWF0IjogMTUzMjQxNjg0OSwKICAiZXhwIjogMjE0NzQ4MzY0NywKICAiaXNzIjogImh0dHA6Ly9wYWFzLmxvY2FsaG9zdDo4MDgwL3VhYS9vYXV0aC90b2tlbiIsCiAgInppZCI6ICJteS1zdWJhY2NvdW50LXN1YmRvbWFpbi1pZCIsCiAgImF1ZCI6IFtdLAogICJhel9hdHRyIjogeyJleHRlcm5hbF9pZCI6ImFiY2QxMjM0In0KfQ.en8xh3IRKia1xD9dTXQsAE7S5g_8AWrcxPxDJjj-aGoKx2yQtlCuykxH8LaSkSrZh5xAtKimHmKY9LtZr1LJvEI5HAae0nkez0YQHUR9au0ZCTWU5YVUb5O8OdoEGQLSjfCiBpky-9udsEqq_Gw7HGr_CDPR0jSO3rBPNqcevFmiPkKsGMm6ZmTg0OIJCXjYOlYdFC3Rq-_BlZvstqRMDIbbL1SO5uZntSQplw41y4oqd_5mF8uVndbzZBSPGqxhfxvYsLOX_Q3DDi9ykFaOs1L6WIndziRmvRNJ3FEHiYDFypSuG2zgNXzI-6QWqi7om-eiAPXPUP72-tvB6gRRUg
\ No newline at end of file
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
index 1e19b81c..a18b0d74 100644
--- 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
@@ -1,19 +1,23 @@
 package com.sap.cloud.security.xsuaa.extractor;
 
+import com.sap.cloud.security.xsuaa.jwt.DecodedJwt;
 import org.apache.commons.io.IOUtils;
+import org.json.JSONException;
 import org.junit.Before;
 import org.junit.Test;
+import org.springframework.security.oauth2.jwt.Jwt;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.*;
 
 public class TokenUtilTest {
 
 	private static String encodedIasToken;
 	private static String encodedXsuaaToken;
+	private static final String invalidToken = "eyJqa3UiOiJodHRwOi8vbG9jYWxob3N0OjY0MzEyL3Rva2VuX2tleXMiLCJraWQiOiJkZWZhdWx0LWtpZCIsImFsZyI6IkhTMjU2In0.eyJjbGllbnRfaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIiwiY2lkIjoic2ItamF2YS1oZWxsby13b3JsZCIsImF6cCI6InNiLWphdmEtaGVsbG8td29ybGQiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvdWFhL29hdXRoL3Rva2VuIiwiemlkIjoidWFhIn0.6cACH-Z8Kr0p6prBYd2gp7nEfOJsA4OsXO_Hkj99XyU";
 
 	@Before
 	public void setup() throws IOException {
@@ -36,4 +40,19 @@ public class TokenUtilTest {
 		assertFalse(TokenUtil.isIasToXsuaaXchangeEnabled());
 	}
 
+	@Test
+	public void parseXsuaaTest() {
+		Jwt jwt = TokenUtil.parseJwt(TokenUtil.decodeJwt(encodedXsuaaToken));
+		assertEquals(2, jwt.getHeaders().size());
+		assertEquals(20, jwt.getClaims().size());
+		assertEquals(jwt.getIssuedAt(), Instant.ofEpochSecond(1442912244));
+		assertEquals(jwt.getExpiresAt(), Instant.ofEpochSecond(1603468794));
+	}
+
+	@Test
+	public void tokenWithMissingExpClaim() {
+		DecodedJwt decodedJwt = TokenUtil.decodeJwt(invalidToken);
+		assertThrows(JSONException.class, () -> TokenUtil.parseJwt(decodedJwt));
+	}
+
 }
\ No newline at end of file