Repository: SAP/cloud-security-xsuaa-integration
Pull request #325 (2020-07-06 09:31:18)

XsuaaToken should provide method to access external attributes

nenaraab: @hassler-d 
good point, i will provide that as part of the API as it is more generic....
getSubdomain/getSubaccountId remains in XsuaaToken w/o API

Patch:
diff --git a/java-api/pom.xml b/java-api/pom.xml
index 836b0545..8f6e0f3c 100644
--- a/java-api/pom.xml
+++ b/java-api/pom.xml
@@ -53,6 +53,11 @@
 			<artifactId>commons-io</artifactId>
 			<scope>test</scope>
 		</dependency>
+		<dependency> <!-- check if it's still needed when slf4j-api 1.8 is available -->
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-simple</artifactId>
+			<scope>test</scope>
+		</dependency>
     </dependencies>
 
 	<build>
diff --git a/java-api/src/main/java/com/sap/cloud/security/config/Service.java b/java-api/src/main/java/com/sap/cloud/security/config/Service.java
index 24e5a8e3..37a5b0ea 100644
--- a/java-api/src/main/java/com/sap/cloud/security/config/Service.java
+++ b/java-api/src/main/java/com/sap/cloud/security/config/Service.java
@@ -28,7 +28,7 @@ public enum Service {
 	 */
 	public String getCFName() {
 		if (this == IAS && cloudFoundryName == null) {
-			LoggerFactory.getLogger(Service.class).error("IAS Service is not yet supported!!!"); // TODO remove
+			LoggerFactory.getLogger(Service.class).info("IAS Service is not yet supported!!!"); // TODO remove
 		}
 		return cloudFoundryName;
 	}
diff --git a/java-api/src/main/java/com/sap/cloud/security/token/AccessToken.java b/java-api/src/main/java/com/sap/cloud/security/token/AccessToken.java
index bcf23e65..383f2dc7 100644
--- a/java-api/src/main/java/com/sap/cloud/security/token/AccessToken.java
+++ b/java-api/src/main/java/com/sap/cloud/security/token/AccessToken.java
@@ -2,6 +2,7 @@ package com.sap.cloud.security.token;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import java.util.Optional;
 import java.util.Set;
 
 /**
@@ -45,4 +46,26 @@ public interface AccessToken extends Token {
 	 **/
 	@Nullable
 	GrantType getGrantType();
+
+	/**
+	 * Returns the String value of attribute of claim. <br>
+	 * <code>
+	 *     "claimName": {
+	 *         "attributeName": "attributeValueAsString"
+	 *     },
+	 *     </code> Example: <br>
+	 * <code>
+	 *     import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
+	 *
+	 *     token.getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, EXTERNAL_ATTRIBUTE_SUBACCOUNTID);
+	 *     </code>
+	 * 
+	 * @return the String value of attribute of claim.
+	 **/
+	@Nullable
+	default String getAttributeFromClaimAsString(String claimName, String attributeName) {
+		return Optional.ofNullable(getClaimAsJsonObject(claimName))
+				.map(claim -> claim.getAsString(attributeName))
+				.orElse(null);
+	}
 }
diff --git a/java-security/Migration_SAPJavaBuildpackProjects.md b/java-security/Migration_SAPJavaBuildpackProjects.md
index 030f0039..aaba9fd7 100644
--- a/java-security/Migration_SAPJavaBuildpackProjects.md
+++ b/java-security/Migration_SAPJavaBuildpackProjects.md
@@ -43,9 +43,29 @@ Whether the token is issued for your application or not is now validated by the
 
 ### Congratulation! With that you're Done!
 
+### Troubleshooting
 
-### Get stuck with migration
-[Open an issue on Github](https://github.com/SAP/cloud-security-xsuaa-integration/issues/new) and provide details like client-lib / migration guide / issue you’re facing.
+If you run into issues with the migration of your application, you can increase the logging level. 
+This helps to identify the root cause of the issue. In the buildpack this can be done by setting the `SET_LOGGING_LEVEL`
+environment variable for your application. You can do this in the `manifest.yml` with the `env` option like so:
+
+```yaml
+env:
+    SET_LOGGING_LEVEL: '{com.sap.xs.security: DEBUG, com.sap.cloud.security: DEBUG}'
+```
+After you have made changes to the `manifest.yml` you need do re-deploy your app.
+This sets the logging level for this library and the security parts of the buildpack to `DEBUG`.
+For a running application this can also be done with the `cf` command line tool:
+
+```shell
+cf set-env <your app name> SET_LOGGING_LEVEL "{com.sap.xs.security: DEBUG, com.sap.cloud.security: DEBUG}"
+```
+
+You need to restage your application for the changes to take effect.
+
+If you are still stuck with migration, please
+[open an issue on Github](https://github.com/SAP/cloud-security-xsuaa-integration/issues/new)
+and provide details like client-lib / migration guide / issue you’re facing and, when available, (debug) logs.
 
 ### [OPTIONAL] Leverage new API and features
 You can continue [here](Migration_SAPJavaBuildpackProjects_V2.md) to understand what needs to be done to leverage the new `java-api` that is exposed by the SAP Java Buildpack as of version `1.26.1`.
diff --git a/java-security/src/main/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilder.java b/java-security/src/main/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilder.java
index 45d0f1b5..4b78c323 100644
--- a/java-security/src/main/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilder.java
+++ b/java-security/src/main/java/com/sap/cloud/security/config/OAuth2ServiceConfigurationBuilder.java
@@ -166,5 +166,14 @@ public class OAuth2ServiceConfigurationBuilder {
 		public int hashCode() {
 			return Objects.hash(properties, runInLegacyMode, service);
 		}
+
+		@Override
+		public String toString() {
+			return "OAuth2ServiceConfigurationImpl{" +
+					"properties=" + properties +
+					", service=" + service +
+					", legacyMode=" + isLegacyMode() +
+					'}';
+		}
 	}
 }
\ No newline at end of file
diff --git a/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java b/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java
index 1c206505..b2abea12 100644
--- a/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java
+++ b/java-security/src/main/java/com/sap/cloud/security/json/DefaultJsonObject.java
@@ -190,4 +190,8 @@ public class DefaultJsonObject implements JsonObject {
 		}
 	}
 
+	@Override
+	public String toString() {
+		return jsonObject.toString(2);
+	}
 }
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 f80dc479..61028ca1 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
@@ -27,12 +27,14 @@ import static com.sap.cloud.security.token.TokenClaims.XSUAA.ISSUED_AT;
  * header parameters and claims.
  */
 public abstract class AbstractToken implements Token {
+	private final DecodedJwt decodedJwt;
 	protected final DefaultJsonObject tokenHeader;
 	protected final DefaultJsonObject tokenBody;
-	protected final String jwtToken;
 
 	public AbstractToken(@Nonnull DecodedJwt decodedJwt) {
-		this(decodedJwt.getHeader(), decodedJwt.getPayload(), decodedJwt.getEncodedToken());
+		this.tokenHeader = new DefaultJsonObject(decodedJwt.getHeader());
+		this.tokenBody = new DefaultJsonObject(decodedJwt.getPayload());
+		this.decodedJwt = decodedJwt;
 	}
 
 	/**
@@ -47,12 +49,6 @@ public abstract class AbstractToken implements Token {
 		this(Base64JwtDecoder.getInstance().decode(removeBearer(jwtToken)));
 	}
 
-	AbstractToken(String jsonHeader, String jsonPayload, String jwtToken) {
-		tokenHeader = new DefaultJsonObject(jsonHeader);
-		tokenBody = new DefaultJsonObject(jsonPayload);
-		this.jwtToken = jwtToken;
-	}
-
 	@Nullable
 	@Override
 	public String getHeaderParameterAsString(@Nonnull String headerName) {
@@ -109,7 +105,7 @@ public abstract class AbstractToken implements Token {
 
 	@Override
 	public String getTokenValue() {
-		return jwtToken;
+		return decodedJwt.getEncodedToken();
 	}
 
 	@Override
@@ -168,4 +164,9 @@ public abstract class AbstractToken implements Token {
 	public String getZoneId() {
 		return getClaimAsString(TokenClaims.SAP_GLOBAL_ZONE_ID);
 	}
+
+	@Override
+	public String toString() {
+		return decodedJwt.toString();
+	}
 }
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java b/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java
index 49e90e07..4470644c 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java
@@ -172,13 +172,6 @@ public class XsuaaToken extends AbstractToken implements AccessToken {
 				.orElse(getClaimAsString(ZONE_ID));
 	}
 
-	@Nullable
-	private String getAttributeFromClaimAsString(String claimName, String attributeName) {
-		return Optional.ofNullable(getClaimAsJsonObject(claimName))
-				.map(claim -> claim.getAsString(attributeName))
-				.orElse(null);
-	}
-
 	@Override
 	public String getZoneId() {
 		return Objects.nonNull(super.getZoneId()) ? super.getZoneId() : getClaimAsString(ZONE_ID);
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/CombiningValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/CombiningValidator.java
index bdec1599..a1358d97 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/validation/CombiningValidator.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/CombiningValidator.java
@@ -1,11 +1,10 @@
 package com.sap.cloud.security.token.validation;
 
 import com.sap.cloud.security.xsuaa.Assertions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 /**
  * This is a special validator that combines several validators into one. By
@@ -16,6 +15,7 @@ import java.util.Set;
  */
 public class CombiningValidator<T> implements Validator<T> {
 
+	private static final Logger LOGGER = LoggerFactory.getLogger(CombiningValidator.class);
 	private final List<Validator<T>> validators;
 	private final Set<ValidationListener> validationListeners = new HashSet<>();
 
@@ -34,6 +34,7 @@ public class CombiningValidator<T> implements Validator<T> {
 		for (Validator<T> validator : validators) {
 			ValidationResult result = validator.validate(t);
 			if (result.isErroneous()) {
+				debugLog(t, validator);
 				validationListeners.forEach(listener -> listener.onValidationError(result));
 				return result;
 			}
@@ -41,18 +42,6 @@ public class CombiningValidator<T> implements Validator<T> {
 		return createValidationResult();
 	}
 
-	private ValidationResult createValidationResult() {
-		if (validators.isEmpty()) {
-			ValidationResult result = ValidationResults
-					.createInvalid("CombiningValidator must contain at least one validator!");
-			validationListeners.forEach(listener -> listener.onValidationError(result));
-			return result;
-		} else {
-			validationListeners.forEach(ValidationListener::onValidationSuccess);
-			return ValidationResults.createValid();
-		}
-	}
-
 	public List<Validator<T>> getValidators() {
 		return validators;
 	}
@@ -88,4 +77,25 @@ public class CombiningValidator<T> implements Validator<T> {
 	public void removeValidationListener(ValidationListener validationListener) {
 		validationListeners.remove(validationListener);
 	}
+
+	private void debugLog(T t, Validator<T> validator) {
+		if (LOGGER.isDebugEnabled()) {
+			String objectType = t == null ? "null" : t.getClass().getName();
+			LOGGER.debug("Validator that caused the failed validation: {}", validator.getClass().getName());
+			LOGGER.debug("Object of type {} that caused the failed validation: {}{}", objectType,
+					System.lineSeparator(), t);
+		}
+	}
+
+	private ValidationResult createValidationResult() {
+		if (validators.isEmpty()) {
+			ValidationResult result = ValidationResults
+					.createInvalid("CombiningValidator must contain at least one validator!");
+			validationListeners.forEach(listener -> listener.onValidationError(result));
+			return result;
+		} else {
+			validationListeners.forEach(ValidationListener::onValidationSuccess);
+			return ValidationResults.createValid();
+		}
+	}
 }
diff --git a/samples/sap-java-buildpack-api-usage/manifest.yml b/samples/sap-java-buildpack-api-usage/manifest.yml
index 7ea2000f..be769dd1 100644
--- a/samples/sap-java-buildpack-api-usage/manifest.yml
+++ b/samples/sap-java-buildpack-api-usage/manifest.yml
@@ -17,6 +17,7 @@ applications:
   services:
     - xsuaa-buildpack
   env:
+    SET_LOGGING_LEVEL: '{com.sap.xs.security: DEBUG, com.sap.cloud.security: DEBUG}'
     ENABLE_SECURITY_JAVA_API_V2: true
 # Application Router as web server
 - name: approuter-sap-java-buildpack-api-usage


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/AbstractTokenTest.java b/java-security/src/test/java/com/sap/cloud/security/token/AbstractTokenTest.java
index 354838ba..ebdc44fd 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/AbstractTokenTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/AbstractTokenTest.java
@@ -124,4 +124,19 @@ public class AbstractTokenTest {
 		assertThat(cut.getClaimAsJsonObject("doesNotExist")).isNull();
 	}
 
+	@Test
+	public void toString_doesNotContainEncodedToken() {
+		assertThat(cut.toString()).doesNotContain(cut.getTokenValue());
+	}
+
+	@Test
+	public void toString_containsTokenContent() {
+		assertThat(cut.toString())
+				.contains(cut.getHeaderParameterAsString(TokenHeader.JWKS_URL))
+				.contains(cut.getHeaderParameterAsString(TokenHeader.KEY_ID))
+				.contains(cut.getAudiences())
+				.contains(cut.getClaimAsString(TokenClaims.XSUAA.CLIENT_ID))
+				.contains(cut.getClaimAsString(TokenClaims.XSUAA.GRANT_TYPE))
+				.contains(cut.getClaimAsStringList(TokenClaims.XSUAA.SCOPES));
+	}
 }
\ No newline at end of file
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java b/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java
index 0d741cc8..d6467fbb 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java
@@ -7,6 +7,7 @@ import org.junit.Test;
 import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.util.Optional;
 
 import static java.nio.charset.StandardCharsets.*;
 import static org.assertj.core.api.Assertions.assertThat;
@@ -131,6 +132,14 @@ public class XsuaaTokenTest {
 		assertThat(clientCredentialsToken.getSubaccountId()).isEqualTo("uaa");
 	}
 
+	@Test
+	public void readCloneServiceInstanceId() {
+		String authHeader = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHRfYXR0ciI6eyJ6ZG4iOiJ0ZXN0c3ViZG9tYWluIiwic2VydmljZWluc3RhbmNlaWQiOiJhYmNkMTIzNCJ9LCJ6aWQiOiJlMmY3ZmJkYi0wMzI2LTQwZTYtOTQwZi1kZmRkYWQwNTdmZjMiLCJncmFudF90eXBlIjoidXJuOmlldGY6cGFyYW1zOm9hdXRoOmdyYW50LXR5cGU6c2FtbDItYmVhcmVyIiwidXNlcl9uYW1lIjoidGVzdFVzZXIiLCJvcmlnaW4iOiJ1c2VySWRwIiwiZXhwIjo2OTc0MDMxNjAwLCJpYXQiOjE1OTM3ODQ2MDcsImVtYWlsIjoidGVzdFVzZXJAdGVzdC5vcmciLCJjaWQiOiJzYi1qYXZhLWhlbGxvLXdvcmxkIn0.yB_ALtO_shdJJkXeRSxKFRVghDmrxdnZ1-WssO_hQ9AAuaQX-rA6eMwuikjcWhkzWnjBzQg4LO8aLQhtUUIS60cpXZG_zP7y9iULCxQQt2vTMbVC0unHB2ytBf2GWbIq_WplfNwshZmbt2ETDClz87VBla-yG6rIZdAio4jZuLTQzJAVCMhJCkmhuMedzbrzGovpblT49UIbi3v4cxhBuoHhAZVfPEUX4-22BTcgFFVlOySvDn6xBVLVqBcjOl_JGjFcWPImj0BqdEuaQq2-A3_F_XKhQf_AK7rLF7kvaIZ2k3i-1GWapqLZsCed23Ihce5m-cedv3857YQPZxAHZw";
+		XsuaaToken token = new XsuaaToken(authHeader);
+		assertThat(token.getAttributeFromClaimAsString(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE, "serviceinstanceid"))
+				.isEqualTo("abcd1234");
+	}
+
 	@Test
 	public void getSubaccountId_noSubaccountIdAndNoFallback_isNull() {
 		assertThat(userToken.getSubaccountId()).isNull();
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtAudienceValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtAudienceValidatorTest.java
index 9e31d0cf..706f5ff5 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtAudienceValidatorTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtAudienceValidatorTest.java
@@ -73,7 +73,6 @@ public class JwtAudienceValidatorTest {
 	@Test
 	public void validate_tokenClientIdMatchesTrustedClientId() {
 		Mockito.when(token.getAudiences()).thenReturn(Collections.emptySet());
-		Mockito.when(token.getClaimAsStringList(TokenClaims.XSUAA.SCOPES)).thenReturn(Collections.EMPTY_LIST);
 
 		// configures audience validator with client-id from VCAP_SERVICES
 		ValidationResult result = new JwtAudienceValidator("client")
@@ -114,7 +113,7 @@ public class JwtAudienceValidatorTest {
 	public void validate_tokenClientIdDoesNotMatchTrustedBrokerClientId() {
 		Mockito.when(token.getAudiences()).thenReturn(Collections.emptySet());
 		Mockito.when(token.getClaimAsString(TokenClaims.XSUAA.CLIENT_ID))
-				.thenReturn("sb-clone-app-id!b123|-" + XSUAA_BROKER_XSAPPNAME);
+				.thenReturn("sb-clone-app-id!b123|xxx" + XSUAA_BROKER_XSAPPNAME);
 
 		// configures audience validator with client-id from VCAP_SERVICES
 		ValidationResult result = new JwtAudienceValidator(XSUAA_BROKER_XSAPPNAME)
@@ -193,4 +192,5 @@ public class JwtAudienceValidatorTest {
 		assertThat(audiences).containsExactlyInAnyOrder("test1!t1", "client", "xsappid");
 	}
 
+
 }
\ No newline at end of file