Repository: SAP/cloud-security-xsuaa-integration
Pull request #336 (2020-07-14 14:36:11)

Python script 2fa
export ENABLE_2_FACTOR=x


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 94a5eaba..7c47a9fa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file.
 - [java-security] provide debug logs for failing token validation, see [troubleshoot](/java-security/README.md#troubleshoot).
 - [java-security-test] Fixed default value for jku token header to `http://localhost/token_keys`
 - [samples] Upgraded [approuter](https://www.npmjs.com/package/@sap/approuter) version to "^8.2.0"
+- [spring-xsuaa-starter] Upgrade Spring versions:
+  - spring.boot.version: 2.3.0.RELEASE --> 2.3.1.RELEASE
+  - spring.core.version: 5.2.6.RELEASE --> 5.2.7.RELEASE
+  - spring.security.version: 5.3.2.RELEASE --> 5.3.3.RELEASE
 
 ## 2.7.4
 - [java-security] Audience Validation validates to true when the derived `client_id` of broker-clone token matches the trusted client. This is relevant to support tokens of grant type `user_token` that contains no scopes.
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 b18e73b0..07abfc33 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
@@ -1,7 +1,10 @@
 package com.sap.cloud.security.token;
 
+import com.sap.cloud.security.json.JsonObject;
+
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 
@@ -53,7 +56,7 @@ public interface AccessToken extends Token {
 	 *     "claimName": {
 	 *         "attributeName": "attributeValueAsString"
 	 *     },
-	 *     </code><br>
+	 *     </code><br><br>
 	 * Example: <br>
 	 * <code>
 	 *     import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
@@ -61,7 +64,7 @@ public interface AccessToken extends Token {
 	 *     token.getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, EXTERNAL_ATTRIBUTE_SUBACCOUNTID);
 	 *     </code>
 	 * 
-	 * @return the String value of a claim attribute.
+	 * @return the String value of a claim attribute or null if claim or its attribute does not exist.
 	 **/
 	@Nullable
 	default String getAttributeFromClaimAsString(String claimName, String attributeName) {
@@ -69,4 +72,28 @@ public interface AccessToken extends Token {
 				.map(claim -> claim.getAsString(attributeName))
 				.orElse(null);
 	}
+
+	/**
+	 * Returns the String list of a claim attribute. <br>
+	 * <code>
+	 *     "claimName": {
+	 *         "attributeName": ["attributeValueAsString", "attributeValue2AsString"]
+	 *     },
+	 *     </code><br><br>
+	 * Example: <br>
+	 * <code>
+	 *     import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;
+	 *
+	 *     token.getAttributeFromClaimAsString(XS_USER_ATTRIBUTES, "custom_role");
+	 *     </code>
+	 *
+	 * @return the String value of a claim attribute or null if claim or its attribute does not exist.
+	 **/
+	@Nullable
+	default List<String> getAttributeFromClaimAsStringList(String claimName, String attributeName) {
+		JsonObject claimAsJsonObject = getClaimAsJsonObject(claimName);
+		return Optional.ofNullable(claimAsJsonObject)
+				.map(jsonObject -> jsonObject.getAsList(attributeName, String.class))
+				.orElse(null);
+	}
 }
diff --git a/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java b/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java
index d71d7158..bedb8c93 100644
--- a/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java
+++ b/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java
@@ -38,7 +38,7 @@ public final class TokenClaims {
 		public static final String EXTERNAL_ATTRIBUTE_SUBACCOUNTID = "subaccountid";
 		@Deprecated
 		public static final String EXTERNAL_ATTRIBUTE_ENHANCER = "enhancer";
-
+		public static final String XS_USER_ATTRIBUTES = "xs.user.attributes";
 		public static final String TRUSTED_CLIENT_ID_SUFFIX = "trustedclientidsuffix";
 	}
 
diff --git a/java-security/Migration_SAPJavaBuildpackProjects_V2.md b/java-security/Migration_SAPJavaBuildpackProjects_V2.md
index 5ace9925..31a7ebbe 100644
--- a/java-security/Migration_SAPJavaBuildpackProjects_V2.md
+++ b/java-security/Migration_SAPJavaBuildpackProjects_V2.md
@@ -71,9 +71,10 @@ This would again require a dependency to the legacy api:
 ```
 
 #### Option 1: UserInfoHolder
-It is also still possible to use the `UserInfoHolder` to obtain the `XSUserInfo` object. 
+It is also still possible to use the `UserInfoHolder` to obtain the `XSUserInfo` object.
+
 #### Option 2: XSUserInfoAdapter
-You can use `XSUserInfoAdapter`:
+You can use `XSUserInfoAdapter` which requires additionally `com.sap.cloud.security:java-security`:
 ```java
 try {
 	XSUserInfo userInfo = new XSUserInfoAdapter(token);
@@ -83,7 +84,6 @@ try {
 }
 ```
 
-
 ### New Feature: SecurityContext <a name="security-context"></a>
 In order to obtain token information from the thread local storage (instead of the request) you can use `SecurityContext` from [`java-api`](#maven).
 
diff --git a/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java b/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
index 24d41128..d16360ae 100644
--- a/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
+++ b/java-security/src/main/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapter.java
@@ -40,7 +40,6 @@ public class XSUserInfoAdapter implements XSUserInfo {
 
 	static final String EXTERNAL_CONTEXT = "ext_ctx";
 	static final String CLAIM_ADDITIONAL_AZ_ATTR = "az_attr";
-	static final String XS_USER_ATTRIBUTES = "xs.user.attributes";
 	static final String XS_SYSTEM_ATTRIBUTES = "xs.system.attributes";
 	static final String HDB_NAMEDUSER_SAML = "hdb.nameduser.saml";
 	static final String SERVICEINSTANCEID = "serviceinstanceid";
@@ -192,7 +191,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 		if (name.equals(HDB)) {
 			String token;
 			if (accessToken.hasClaim(EXTERNAL_CONTEXT)) {
-				token = getAttributeFromClaimAsString(EXTERNAL_CONTEXT, HDB_NAMEDUSER_SAML);
+				token = accessToken.getAttributeFromClaimAsString(EXTERNAL_CONTEXT, HDB_NAMEDUSER_SAML);
 			} else {
 				token = accessToken.getClaimAsString(HDB_NAMEDUSER_SAML);
 			}
@@ -247,7 +246,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 
 	@Override
 	public String getAdditionalAuthAttribute(String attributeName) {
-		return Optional.ofNullable(getAttributeFromClaimAsString(CLAIM_ADDITIONAL_AZ_ATTR, attributeName))
+		return Optional.ofNullable(accessToken.getAttributeFromClaimAsString(CLAIM_ADDITIONAL_AZ_ATTR, attributeName))
 				.orElseThrow(createXSUserInfoException(attributeName));
 	}
 
@@ -400,9 +399,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	private String[] getMultiValueAttributeFromExtObject(String claimName, String attributeName) {
-		JsonObject claimAsJsonObject = getClaimAsJsonObject(claimName);
-		return Optional.ofNullable(claimAsJsonObject)
-				.map(jsonObject -> jsonObject.getAsList(attributeName, String.class))
+		return Optional.ofNullable(accessToken.getAttributeFromClaimAsStringList(claimName, attributeName))
 				.map(values -> values.toArray(new String[] {}))
 				.orElseThrow(createXSUserInfoException(attributeName));
 	}
@@ -415,12 +412,6 @@ public class XSUserInfoAdapter implements XSUserInfo {
 		}
 	}
 
-	@Nullable
-	private String getAttributeFromClaimAsString(String claimName, String attributeName) {
-		return Optional.ofNullable(getClaimAsJsonObject(claimName))
-				.map(claim -> claim.getAsString(attributeName)).orElse(null);
-	}
-
 	private Supplier<XSUserInfoException> createXSUserInfoException(String attribute) {
 		return () -> new XSUserInfoException("Invalid user attribute " + attribute);
 	}
@@ -443,7 +434,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 	}
 
 	String getExternalAttribute(String attributeName) {
-		return getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, attributeName);
+		return accessToken.getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, attributeName);
 	}
 
 	/**
@@ -504,7 +495,7 @@ public class XSUserInfoAdapter implements XSUserInfo {
 					.execute().getAccessToken();
 		} catch (TokenFlowException e) {
 			LOGGER.error("Error performing Client Credentials Flow", e);
-			throw new XSUserInfoException("Error performing Client Credentials Flow..", e);
+			throw new XSUserInfoException("Error performing Client Credentials Flow.", e);
 		}
 		return ccfToken;
 	}
diff --git a/pom.xml b/pom.xml
index 530b4c37..fa3bdae5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,15 +56,15 @@
 		<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.0.RELEASE</spring.boot.version>
-		<spring.core.version>5.2.6.RELEASE</spring.core.version>
-		<spring.security.version>5.3.2.RELEASE</spring.security.version>
-		<spring.security.oauth2.version>2.4.1.RELEASE</spring.security.oauth2.version>
-		<spring.security.jwt.version>1.1.0.RELEASE</spring.security.jwt.version>
-		<reactor.version>3.2.8.RELEASE</reactor.version>
-		<log4j.version>2.13.1</log4j.version>
+		<spring.boot.version>2.3.1.RELEASE</spring.boot.version>
+		<spring.core.version>5.2.7.RELEASE</spring.core.version>
+		<spring.security.version>5.3.3.RELEASE</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>
+		<log4j.version>2.13.3</log4j.version>
 		<slf4j.api.version>1.7.30</slf4j.api.version> <!--see also here http://www.slf4j.org/faq.html#changesInVersion18 -->
-		<org.json.version>20190722</org.json.version>
+		<org.json.version>20200518</org.json.version>
 		<google.jsr305.version>3.0.2</google.jsr305.version>
 		<apache.httpclient.version>4.5.9</apache.httpclient.version>
 		<caffeine.version>2.8.2</caffeine.version>
diff --git a/troubleshooting/logcollector/cf-logcollector.ps1 b/troubleshooting/logcollector/cf-logcollector.ps1
index a717c286..8b92e93d 100644
--- a/troubleshooting/logcollector/cf-logcollector.ps1
+++ b/troubleshooting/logcollector/cf-logcollector.ps1
@@ -18,17 +18,12 @@
 param(
     [Parameter(Mandatory = $True, HelpMessage = "Enter application name.", Position = 0)]
     [String]$App,
-    [Parameter(Mandatory = $True, HelpMessage = "Enter application router name.", Position = 1)]
+    [Parameter(Mandatory = $True, HelpMessage = "Enter application router application name.", Position = 1)]
     [String]$Approuter,
-<<<<<<< HEAD
-    [Parameter(Mandatory = $false, HelpMessage = "Enter path to output zip file.", Position = 2)]
-    [String]$Logs = "$HOME\logcollection.zip"
-=======
     [Parameter(Mandatory=$false, HelpMessage="Enter path to output zip file.", Position=2)]
     [String]$Logs="$HOME\logcollection.zip",
     [Parameter(Mandatory=$false, HelpMessage="Restore log-levels only?", Position=2)]
     [Switch]$RestoreLogLevelsOnly=$false
->>>>>>> ias
 )
 
 
@@ -77,23 +72,6 @@ if (-Not (Get-Command cf)) {
     break
 }
 
-<<<<<<< HEAD
-#login to the correct API endpoint
-Write-Host "`n Did you log on and target the right space already?" -ForegroundColor Cyan -NoNewline;
-$Title = ""
-$Info = "Choose?"
-$options = [System.Management.Automation.Host.ChoiceDescription[]] @("&Yes", "&No")
-[int]$defaultchoice = 0
-$opt = $host.UI.PromptForChoice($Title, $Info, $Options, $defaultchoice)
-switch ($opt) {
-    0 {      
-        # just continue
-    }
-    1 {
-        Write-Host "`nLogging in...`n"
-        cflogin
-        Write-Host "`nSuccessfully logged in, will continue..."          
-=======
 Write-Host "`nAre you already logged in to the CF space you want to work on?`n"
 $Title = ""
 $Info = "Should we log you on?"
@@ -105,7 +83,6 @@ switch($opt){
       cflogin
     }
     1 {        
->>>>>>> ias
     }
 }
 
@@ -156,4 +133,4 @@ restoreloglevelsandrestart
 #End
 Write-Host "`nAll done. " -ForegroundColor Green -NoNewline
 Write-Host "Your file is here: "
-Resolve-Path $Logs
\ No newline at end of file
+Resolve-Path $Logs


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
index 8e05f25a..80e8d05d 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/xs/XSUserInfoAdapterTest.java
@@ -23,6 +23,7 @@ import java.io.IOException;
 import static com.sap.cloud.security.adapter.xs.XSUserInfoAdapter.*;
 import static com.sap.cloud.security.config.cf.CFConstants.XSUAA.IDENTITY_ZONE;
 import static com.sap.cloud.security.token.TokenClaims.XSUAA.TRUSTED_CLIENT_ID_SUFFIX;
+import static com.sap.cloud.security.token.TokenClaims.XSUAA.XS_USER_ATTRIBUTES;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -174,14 +175,11 @@ public class XSUserInfoAdapterTest {
 
 	@Test
 	public void getToken_fromExternalContext() throws XSUserInfoException {
-		String internalToken = "token";
-		JsonObject externalContextMock = mock(JsonObject.class);
-		when(externalContextMock.getAsString(HDB_NAMEDUSER_SAML)).thenReturn(internalToken);
-		XsuaaToken mockToken = createMockToken(externalContextMock);
+		XsuaaToken mockToken = createMockToken("token");
 
 		cut = new XSUserInfoAdapter(mockToken);
 
-		assertThat(cut.getToken(XSUserInfoAdapter.SYSTEM, XSUserInfoAdapter.HDB)).isEqualTo(internalToken);
+		assertThat(cut.getToken(XSUserInfoAdapter.SYSTEM, XSUserInfoAdapter.HDB)).isEqualTo("token");
 	}
 
 	@Test
@@ -526,10 +524,11 @@ public class XSUserInfoAdapterTest {
 		return createMockToken(GrantType.SAML2_BEARER);
 	}
 
-	private XsuaaToken createMockToken(JsonObject externalContext) {
+	private XsuaaToken createMockToken(String internalToken) {
 		final XsuaaToken mockToken = createMockToken();
 		when(mockToken.hasClaim(EXTERNAL_CONTEXT)).thenReturn(true);
-		when(mockToken.getClaimAsJsonObject(EXTERNAL_CONTEXT)).thenReturn(externalContext);
+		when(mockToken.getAttributeFromClaimAsString(EXTERNAL_CONTEXT, HDB_NAMEDUSER_SAML)).thenReturn(internalToken);
+
 		return mockToken;
 	}
 
diff --git a/samples/deploy_and_test.py b/samples/deploy_and_test.py
index c6bde8a0..915490a7 100755
--- a/samples/deploy_and_test.py
+++ b/samples/deploy_and_test.py
@@ -90,7 +90,7 @@ class SampleTest(abc.ABC, unittest.TestCase):
 
     def perform_get_request(self, path, username=None, password=None):
         if (username is not None and password is not None):
-            authorization_value = b64encode(bytes(username + ':' + password, 'utf-8')).decode("ascii")
+            authorization_value = b64encode(bytes(username + ':' + password + self.__get_2factor_auth_code(), 'utf-8')).decode("ascii")
             return self.__perform_get_request(path=path, additional_headers={'Authorization': 'Basic ' + authorization_value})
         return self.__perform_get_request(path=path)
 
@@ -118,7 +118,7 @@ class SampleTest(abc.ABC, unittest.TestCase):
             clientsecret=deployed_app.clientsecret,
             grant_type='password',
             username=self.credentials.username,
-            password=self.credentials.password)
+            password=self.credentials.password + self.__get_2factor_auth_code())
 
     def __get_api_access(self):
         if (self.__api_access is None):
@@ -139,6 +139,11 @@ class SampleTest(abc.ABC, unittest.TestCase):
         logging.info('Response: ' + str(resp))
         return resp
 
+    def __get_2factor_auth_code(self):
+        auth_code = ""
+        if (os.getenv('ENABLE_2_FACTOR') is not None):
+            auth_code = input("2-Factor Authenticator Code: ") or ""
+        return auth_code
 
 class TestTokenClient(SampleTest):