Repository: SAP/cloud-security-xsuaa-integration
Pull request #234 (2020-02-28 12:54:34)

Improvements for buildpack integration
Various improvements for build pack integration


Patch:
diff --git a/java-api/src/main/java/com/sap/cloud/security/token/SecurityContext.java b/java-api/src/main/java/com/sap/cloud/security/token/SecurityContext.java
index b605a5fa..06efb52d 100644
--- a/java-api/src/main/java/com/sap/cloud/security/token/SecurityContext.java
+++ b/java-api/src/main/java/com/sap/cloud/security/token/SecurityContext.java
@@ -55,6 +55,9 @@ public class SecurityContext {
 	 * Clears the current Token from thread wide storage.
 	 */
 	public static void clearToken() {
+		final Token token = tokenStorage.get();
+		LOGGER.debug("Token of service {} removed from SecurityContext (thread-locally).",
+				token != null ? token.getService() : "null");
 		tokenStorage.remove();
 	}
 
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 0940f7c5..1b017f83 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
@@ -3,6 +3,8 @@ package com.sap.cloud.security.token;
 import com.sap.cloud.security.config.Service;
 import com.sap.cloud.security.xsuaa.Assertions;
 import com.sap.cloud.security.xsuaa.jwt.DecodedJwt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -20,6 +22,7 @@ import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
 public class XsuaaToken extends AbstractToken implements AccessToken {
 	static final String UNIQUE_USER_NAME_FORMAT = "user/%s/%s"; // user/<origin>/<logonName>
 	static final String UNIQUE_CLIENT_NAME_FORMAT = "client/%s"; // client/<clientid>
+	private static final Logger LOGGER = LoggerFactory.getLogger(XsuaaToken.class);
 	private ScopeConverter scopeConverter;
 
 	/**
@@ -43,6 +46,38 @@ public class XsuaaToken extends AbstractToken implements AccessToken {
 		super(accessToken);
 	}
 
+	/**
+	 * Get unique principal name of a user.
+	 *
+	 * @param origin
+	 *            of the access token
+	 * @param userName
+	 *            of the access token
+	 * @return unique principal name or <code>null</code> if origin or user name is
+	 *         <code>null</code> or empty. Origin must also not contain a '/'
+	 *         character.
+	 */
+	static String getUniquePrincipalName(String origin, String userName) {
+		if (isNullOrEmpty(origin)) {
+			LOGGER.warn("origin claim not set in JWT. Cannot create unique user name. Returning null.");
+			return null;
+		}
+		if (isNullOrEmpty(userName)) {
+			LOGGER.warn("user_name claim not set in JWT. Cannot create unique user name. Returning null.");
+			return null;
+		}
+		if (origin.contains("/")) {
+			LOGGER.warn(
+					"Illegal '/' character detected in origin claim of JWT. Cannot create unique user name. Returning null.");
+			return null;
+		}
+		return String.format(UNIQUE_USER_NAME_FORMAT, origin, userName);
+	}
+
+	private static boolean isNullOrEmpty(String string) {
+		return string == null || string.isEmpty();
+	}
+
 	/**
 	 * Configures a scope converter, e.g. required for the
 	 * {@link #hasLocalScope(String)}
@@ -57,31 +92,6 @@ public class XsuaaToken extends AbstractToken implements AccessToken {
 		return this;
 	}
 
-	/**
-	 * Get unique principal name of a user.
-	 *
-	 * @param origin
-	 *            of the access token
-	 * @param userLoginName
-	 *            of the access token
-	 * @return unique principal name
-	 *
-	 * @throws IllegalArgumentException
-	 */
-	static String getUniquePrincipalName(String origin, String userLoginName) {
-		Assertions.assertHasText(origin,
-				"Origin claim not set in JWT. Cannot create unique user name. Returning null.");
-		Assertions.assertHasText(userLoginName,
-				"User login name claim not set in JWT. Cannot create unique user name. Returning null.");
-
-		if (origin.contains("/")) {
-			throw new IllegalArgumentException(
-					"Illegal '/' character detected in origin claim of JWT. Cannot create unique user name. Returing null.");
-		}
-
-		return String.format(UNIQUE_USER_NAME_FORMAT, origin, userLoginName);
-	}
-
 	@Override
 	public Set<String> getScopes() {
 		LinkedHashSet<String> scopes = new LinkedHashSet<>();


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 bece938e..5067dae6 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
@@ -2,6 +2,7 @@ package com.sap.cloud.security.test;
 
 import static com.sap.cloud.security.token.TokenClaims.AUDIENCE;
 import static com.sap.cloud.security.token.TokenHeader.ALGORITHM;
+import static com.sap.cloud.security.token.TokenHeader.JWKS_URL;
 
 import com.sap.cloud.security.config.Service;
 import com.sap.cloud.security.json.JsonObject;
@@ -31,8 +32,8 @@ import java.util.stream.Collectors;
 public class JwtGenerator {
 	public static final Instant NO_EXPIRE_DATE = new GregorianCalendar(2190, 11, 31).getTime().toInstant();
 
-	private static final Logger logger = LoggerFactory.getLogger(JwtGenerator.class);
-
+	private static final Logger LOGGER = LoggerFactory.getLogger(JwtGenerator.class);
+	private static final String DEFAULT_JWKS_URL = "http://localhost";
 	private static final char DOT = '.';
 
 	private final JSONObject jsonHeader = new JSONObject();
@@ -59,9 +60,17 @@ public class JwtGenerator {
 		instance.service = service;
 		instance.signatureCalculator = signatureCalculator;
 		instance.signatureAlgorithm = JwtSignatureAlgorithm.RS256;
+		setTokenDefaults(clientId, instance);
+		return instance;
+	}
+
+	private static void setTokenDefaults(String clientId, JwtGenerator instance) {
+		instance.withHeaderParameter(ALGORITHM, instance.signatureAlgorithm.value());
 		instance.withClaimValue(TokenClaims.XSUAA.CLIENT_ID, clientId);
 		instance.withExpiration(NO_EXPIRE_DATE);
-		return instance;
+		if (instance.service == Service.XSUAA) {
+			instance.withHeaderParameter(JWKS_URL, DEFAULT_JWKS_URL);
+		}
 	}
 
 	/**
@@ -232,26 +241,33 @@ public class JwtGenerator {
 		if (privateKey == null) {
 			throw new IllegalStateException("Private key was not set!");
 		}
-		withHeaderParameter(ALGORITHM, signatureAlgorithm.value());
+
+		createAudienceClaim();
+
+		switch (service) {
+		case IAS:
+			return new SapIdToken(createTokenAsString());
+		case XSUAA:
+			return new XsuaaToken(createTokenAsString());
+		default:
+			throw new UnsupportedOperationException("Identity Service " + service + " is not supported.");
+		}
+	}
+
+	private void createAudienceClaim() {
 		if (service == Service.IAS) {
 			jsonPayload.put(AUDIENCE, jsonPayload.getString(TokenClaims.XSUAA.CLIENT_ID));
 		} else {
 			jsonPayload.put(AUDIENCE, Arrays.asList(jsonPayload.getString(TokenClaims.XSUAA.CLIENT_ID)));
 		}
+	}
+
+	private String createTokenAsString() {
 		String header = base64Encode(jsonHeader.toString().getBytes());
 		String payload = base64Encode(jsonPayload.toString().getBytes());
 		String headerAndPayload = header + DOT + payload;
 		String signature = calculateSignature(headerAndPayload);
-		String token = headerAndPayload + DOT + signature;
-
-		switch (service) {
-		case IAS:
-			return new SapIdToken(token);
-		case XSUAA:
-			return new XsuaaToken(token);
-		default:
-			throw new UnsupportedOperationException("Identity Service " + service + " is not supported.");
-		}
+		return headerAndPayload + DOT + signature;
 	}
 
 	private static byte[] calculateSignature(PrivateKey privateKey, JwtSignatureAlgorithm signatureAlgorithm,
@@ -267,13 +283,13 @@ public class JwtGenerator {
 			return base64Encode(signatureCalculator
 					.calculateSignature(privateKey, signatureAlgorithm, headerAndPayload.getBytes()));
 		} catch (NoSuchAlgorithmException e) {
-			logger.error("Algorithm '{}' not found.", signatureAlgorithm.javaSignature());
+			LOGGER.error("Algorithm '{}' not found.", signatureAlgorithm.javaSignature());
 			throw new UnsupportedOperationException(e);
 		} catch (SignatureException e) {
-			logger.error("Error creating JWT signature.");
+			LOGGER.error("Error creating JWT signature.");
 			throw new UnsupportedOperationException(e);
 		} catch (InvalidKeyException e) {
-			logger.error("Invalid private key.");
+			LOGGER.error("Invalid private key.");
 			throw new UnsupportedOperationException(e);
 		}
 	}
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 caa5a841..0d95ff71 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
@@ -89,18 +89,12 @@ public class XsuaaTokenTest {
 	}
 
 	@Test
-	public void getUniquePrincipalName_raiseIllegalArgumentExceptions() {
-		assertThatThrownBy(() -> {
-			XsuaaToken.getUniquePrincipalName("origin/", "user");
-		}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("/");
-
-		assertThatThrownBy(() -> {
-			XsuaaToken.getUniquePrincipalName("origin", "");
-		}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("User");
-
-		assertThatThrownBy(() -> {
-			XsuaaToken.getUniquePrincipalName("", "user");
-		}).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Origin");
+	public void getUniquePrincipalName_cannotBeCreated_returnsNull() {
+		assertThat(XsuaaToken.getUniquePrincipalName("origin/", "user")).isNull();
+		assertThat(XsuaaToken.getUniquePrincipalName("origin", "")).isNull();
+		assertThat(XsuaaToken.getUniquePrincipalName("", "user")).isNull();
+		assertThat(XsuaaToken.getUniquePrincipalName(null, "user")).isNull();
+		assertThat(XsuaaToken.getUniquePrincipalName("origin", null)).isNull();
 	}
 
 	@Test