Repository: SAP/cloud-security-xsuaa-integration
Pull request #640 (2021-08-24 11:11:34)

SECAUTH-1483
### In `spring-xsuaa` 
**Before**
```java
Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {
		TokenAuthenticationConverter converter = new TokenAuthenticationConverter(xsuaaServiceConfiguration);
		converter.setLocalScopeAsAuthorities(true);
		return converter;
}
```
**New**
```java
Converter<Jwt, AbstractAuthenticationToken> getJwtAuthenticationConverter() {
          return new TokenAuthenticationConverter(new MyCustomAuthoritiesExtractor());
}

private static class MyCustomAuthoritiesExtractor implements AuthoritiesExtractor {

		@Override
		public Collection<GrantedAuthority> getAuthorities(XsuaaToken token) {
			String appId = "";
			if(token.getClientId().startsWith("sb-")) {
				appId = token.getClientId().replaceFirst("sb-", "");
			}
			AuthoritiesExtractor authoritiesExtractor = new LocalAuthoritiesExtractor(appId);
			return authoritiesExtractor.getAuthorities(token);
		}

	}
```
### In `spring-security` 
You can subclass `XsuaaTokenAuthorizationConverter` and overwrite `localScopeAuthorities(Jwt jwt, Collection<String> scopes)`. An example is not yet available.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe1c93d8..69d2222f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,8 @@
 All notable changes to this project will be documented in this file.
 
 ## 2.10.5
-- [token-client] 
+- [token-client]
+  - new method `SSLContextFactory.createKeyStore(ClientIdentity)`
   - `XsuaaTokenFlows` constructor accepts `com.sap.cloud.security.xsuaa.client.ClientCredentials` as argument.
   
 #### Dependency upgrades
diff --git a/env/pom.xml b/env/pom.xml
index 0f7f4019..5f6e09c3 100644
--- a/env/pom.xml
+++ b/env/pom.xml
@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
-
+<!-- SPDX-FileCopyrightText: 2018-2021 SAP SE or an SAP affiliate company and Cloud Security Client Java contributors -->
+<!-- SPDX-License-Identifier: Apache-2.0 -->
 <project
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
         xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
diff --git a/pom.xml b/pom.xml
index 3393d16d..9929a891 100644
--- a/pom.xml
+++ b/pom.xml
@@ -77,7 +77,7 @@
 		<junit-jupiter.version>5.7.2</junit-jupiter.version>
 		<maven-surefire-plugin>3.0.0-M5</maven-surefire-plugin>
 		<hamcrest.version>1.3</hamcrest.version>
-		<mockito.version>3.12.1</mockito.version>
+		<mockito.version>3.12.4</mockito.version>
 		<assertj.version>3.20.2</assertj.version>
 		<reactor.test.version>3.4.9</reactor.test.version>
 		<skipTests>false</skipTests>
diff --git a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverter.java b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverter.java
index 0d4a37dc..d26c8d31 100644
--- a/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverter.java
+++ b/spring-security/src/main/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverter.java
@@ -41,14 +41,18 @@ public class XsuaaTokenAuthorizationConverter implements Converter<Jwt, Abstract
 	}
 
 	protected Collection<GrantedAuthority> localScopeAuthorities(Jwt jwt) {
-		Collection<GrantedAuthority> localScopeAuthorities = new ArrayList<>();
 		Collection<String> scopes = jwt.getClaimAsStringList(TokenClaims.XSUAA.SCOPES);
 		if (scopes == null) {
 			return Collections.emptySet();
 		}
+		return localScopeAuthorities(jwt, scopes);
+	}
+
+	protected Collection<GrantedAuthority> localScopeAuthorities(Jwt jwt, Collection<String> scopes) {
+		Collection<GrantedAuthority> localScopeAuthorities = new ArrayList<>();
 		for (String scope : scopes) {
 			if (scope.startsWith(appId + ".")) {
-				localScopeAuthorities.add(new SimpleGrantedAuthority(scope.replaceFirst(appId + ".", "")));
+				localScopeAuthorities.add(new SimpleGrantedAuthority(scope.substring(appId.length() + 1)));
 			}
 		}
 		return localScopeAuthorities;
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/LocalAuthoritiesExtractor.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/LocalAuthoritiesExtractor.java
index 04865c98..35673ae1 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/LocalAuthoritiesExtractor.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/LocalAuthoritiesExtractor.java
@@ -43,7 +43,7 @@ public class LocalAuthoritiesExtractor implements AuthoritiesExtractor {
 		}
 		return scopes.stream()
 				.filter(scope -> scope.startsWith(appId + "."))
-				.map(scope -> scope.replaceFirst(appId + ".", ""))
+				.map(scope -> scope.substring(appId.length() + 1))
 				.collect(Collectors.toSet());
 	}
 
diff --git a/token-client/README.md b/token-client/README.md
index 54555a4d..b9470523 100644
--- a/token-client/README.md
+++ b/token-client/README.md
@@ -260,36 +260,41 @@ For spring applications using rest template, you can set
 
 ### Common Pitfalls
 
-- As of version ``2.10`` a warning `In productive environment provide well configured HttpClientFactory service.` is exposed to the application log in case the default implementation of ``ClosableRestClient`` is used and not overwritten by an own well-defined one. <br> In case you like to overwrite [`DefaultHttpClientFactory`](/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java) you can register your own implementation of `HttpClientFactory` interface as following:
-
-    - Create a SPI configuration file with name `com.sap.cloud.security.client.HttpClientFactory` in ``src/main/resources/META-INF/services`` directory.  
-    - Enter the fully qualified name of your `HttpClientFactory` implementation class, e.g. `com.mypackage.CustomHttpClientFactory`.
-    - The implementation could look like:  
-    ````java
-    public class DefaultHttpClientFactory implements HttpClientFactory {
-    
-    	public CloseableHttpClient createClient(ClientIdentity clientIdentity) throws HttpClientException {
-    		// here comes your implementation
-    	}
+#### New warning `In productive environment provide well configured HttpClientFactory service.` 
+As of version ``2.10`` a warning `In productive environment provide well configured HttpClientFactory service.` is exposed to the application log in case the default implementation of ``ClosableRestClient`` is used and not overwritten by an own well-defined one. <br> In case you like to overwrite [`DefaultHttpClientFactory`](/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java) you can register your own implementation of `HttpClientFactory` interface as following:
+
+- Create a SPI configuration file with name `com.sap.cloud.security.client.HttpClientFactory` in ``src/main/resources/META-INF/services`` directory.  
+- Enter the fully qualified name of your `HttpClientFactory` implementation class, e.g. `com.mypackage.CustomHttpClientFactory`.
+- The implementation could look like:  
+````java
+public class DefaultHttpClientFactory implements HttpClientFactory {
+
+    public CloseableHttpClient createClient(ClientIdentity clientIdentity) throws HttpClientException {
+        // here comes your implementation
     }
-    ````
-- This module requires the [JSON-Java](https://github.com/stleary/JSON-java) library.
+}
+````
+#### This module requires the [JSON-Java](https://github.com/stleary/JSON-java) library
 If you have classpath related  issues involving JSON you should take a look at the
 [Troubleshooting JSON class path issues](/docs/Troubleshooting_JsonClasspathIssues.md) document.
 
-- `{\"error\":\"unauthorized\",\"error_description\":\"Unable to map issuer, [http://subdomain.localhost:8080/uaa/oauth/token] , to a single registered provider\"}`  
+#### Token exchange `Unable to map issuer`
+```bash
+{\"error\":\"unauthorized\",\"error_description\":\"Unable to map issuer, [http://subdomain.localhost:8080/uaa/oauth/token] , to a single registered provider\"}
+```  
 Token exchange is only supported within the same identity zone/tenant. That means, that you have to call the `/oauth/token` endpoint of the same subdomain, that was used for the original token. This can be achieved by configuring the user token flow the following way:
-    ````
-    tokenFlows.userTokenFlow().token(jwtToken).subdomain(jwtToken.getSubdomain());
-    ````
-
-- For Spring applications error like:
-    ```java
-    java.lang.IllegalStateException: Failed to load ApplicationContext 
-    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfiguration': Unsatisfied dependency expressed through field 'xsuaaTokenFlows'
-    nested exception is java.lang.NoClassDefFoundError: org/apache/http/client/HttpClient
-    ``` 
-    make sure `org.apache.httpcomponents:httpclient` dependency is provided in the POM.
+````
+tokenFlows.userTokenFlow().token(jwtToken).subdomain(jwtToken.getSubdomain());
+````
+
+#### In Spring: `UnsatisfiedDependencyException`
+For Spring applications error like
+```bash
+java.lang.IllegalStateException: Failed to load ApplicationContext 
+Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'securityConfiguration': Unsatisfied dependency expressed through field 'xsuaaTokenFlows'
+nested exception is java.lang.NoClassDefFoundError: org/apache/http/client/HttpClient
+``` 
+indicates that mandatory `org.apache.httpcomponents:httpclient` dependency is missing in your POM.
 
 
 
diff --git a/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java b/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java
index e63fb252..17b55f27 100644
--- a/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java
+++ b/token-client/src/main/java/com/sap/cloud/security/client/DefaultHttpClientFactory.java
@@ -28,13 +28,10 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
 	public CloseableHttpClient createClient(ClientIdentity clientIdentity) throws HttpClientException {
 		LOGGER.warn("In productive environment provide well configured HttpClientFactory service");
 		if (clientIdentity != null && clientIdentity.isCertificateBased()) {
-			LOGGER.debug("Setting up HTTPS client with: certificate: {}\nprivate key: {}\n",
-					clientIdentity.getCertificate(), clientIdentity.getKey());
-
+			LOGGER.debug("Setting up HTTPS client with: certificate: {}\n", clientIdentity.getCertificate());
 			SSLContext sslContext;
 			try {
-				sslContext = SSLContextFactory.getInstance().create(clientIdentity.getCertificate(),
-						clientIdentity.getKey());
+				sslContext = SSLContextFactory.getInstance().create(clientIdentity);
 			} catch (IOException | GeneralSecurityException e) {
 				throw new HttpClientException(
 						String.format("Couldn't set up https client for service provider. %s.",
diff --git a/token-client/src/main/java/com/sap/cloud/security/mtls/SSLContextFactory.java b/token-client/src/main/java/com/sap/cloud/security/mtls/SSLContextFactory.java
index ac502008..6d91b9cf 100644
--- a/token-client/src/main/java/com/sap/cloud/security/mtls/SSLContextFactory.java
+++ b/token-client/src/main/java/com/sap/cloud/security/mtls/SSLContextFactory.java
@@ -5,6 +5,9 @@
  */
 package com.sap.cloud.security.mtls;
 
+import com.sap.cloud.security.config.ClientCertificate;
+import com.sap.cloud.security.config.ClientIdentity;
+import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -23,6 +26,7 @@ import java.util.Base64;
 import java.util.Collection;
 
 import static com.sap.cloud.security.xsuaa.Assertions.assertHasText;
+import static com.sap.cloud.security.xsuaa.Assertions.assertNotNull;
 
 /**
  * Creates a SSLContext (without Bouncy Castle crypto lib).
@@ -46,9 +50,9 @@ public class SSLContextFactory {
 	 * order to support mutual TLS.
 	 *
 	 * @param x509Certificates
-	 *            you can get from your Service Configuration
+	 *            you can get from your Service Configuration {@link OAuth2ServiceConfiguration#getClientIdentity()}
 	 * @param rsaPrivateKey
-	 *            you can get from your Service Configuration
+	 *            you can get from your Service Configuration{@link OAuth2ServiceConfiguration#getClientIdentity()}
 	 * @return a new SSLContext instance
 	 * @throws GeneralSecurityException
 	 *             in case of key parsing errors
@@ -60,21 +64,57 @@ public class SSLContextFactory {
 		assertHasText(x509Certificates, "x509Certificate is required");
 		assertHasText(rsaPrivateKey, "rsaPrivateKey is required");
 
-		SSLContext sslContext = createDefaultSSLContext();
-
-		PrivateKey privateKey = getPrivateKeyFromString(rsaPrivateKey);
-		Certificate[] certificateChain = getCertificatesFromString(x509Certificates);
+		return create(new ClientCertificate(x509Certificates, rsaPrivateKey, null));
+	}
 
-		KeyStore keystore = initializeKeyStore(privateKey, certificateChain);
+	/**
+	 * Creates a SSLContext which can be used to parameterize your Rest client, in
+	 * order to support mutual TLS.
+	 *
+	 * @param clientIdentity
+	 *            you can get from your Service Configuration {@link OAuth2ServiceConfiguration#getClientIdentity()}
+	 * @return a new SSLContext instance
+	 * @throws GeneralSecurityException
+	 *             in case of key parsing errors
+	 * @throws IOException
+	 *             in case of KeyStore initialization errors
+	 */
+	public SSLContext create(ClientIdentity clientIdentity) throws GeneralSecurityException, IOException {
+		assertNotNull(clientIdentity, "clientIdentity must not be null");
+		assertHasText(clientIdentity.getCertificate(), "clientIdentity.getCertificate() must not return null");
+		assertHasText(clientIdentity.getKey(), "clientIdentity.getKey() must not return null");
 
+		KeyStore keystore = createKeyStore(clientIdentity);
 		KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
 		keyManagerFactory.init(keystore, noPassword);
-
+		SSLContext sslContext = createDefaultSSLContext();
 		sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
-
 		return sslContext;
 	}
 
+	/**
+	 * Initializes a KeyStore which can be used to parameterize your Rest client, in
+	 * order to support mutual TLS.
+	 *
+	 * @param clientIdentity
+	 *            you can get from your Service Configuration {@link OAuth2ServiceConfiguration#getClientIdentity()}
+	 * @return a new KeyStore instance
+	 * @throws GeneralSecurityException
+	 *             in case of key parsing errors
+	 * @throws IOException
+	 *             in case of KeyStore initialization errors
+	 */
+	public KeyStore createKeyStore(ClientIdentity clientIdentity) throws GeneralSecurityException, IOException {
+		assertNotNull(clientIdentity, "clientIdentity must not be null");
+		assertHasText(clientIdentity.getCertificate(), "clientIdentity.getCertificate() must not return null");
+		assertHasText(clientIdentity.getKey(), "clientIdentity.getKey() must not return null");
+
+		PrivateKey privateKey = getPrivateKeyFromString(clientIdentity.getKey());
+		Certificate[] certificateChain = getCertificatesFromString(clientIdentity.getCertificate());
+
+		return initializeKeyStore(privateKey, certificateChain);
+	}
+
 	private KeyStore initializeKeyStore(PrivateKey privateKey, Certificate[] certificateChain)
 			throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
 		KeyStore keystore = KeyStore.getInstance("jks");
@@ -146,7 +186,6 @@ public class SSLContextFactory {
 					primeExponentQ,
 					crtCoefficient);
 		} catch (IOException e) {
-			logger.error("Exception during parsing DER encoded private key ({})", e.getMessage());
 			throw new GeneralSecurityException("Exception during parsing DER encoded private key", e);
 		}
 		return keySpec;


Test patch:
diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverterTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverterTest.java
index de0c69b1..1d8d2817 100644
--- a/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverterTest.java
+++ b/spring-security/src/test/java/com/sap/cloud/security/spring/token/authentication/XsuaaTokenAuthorizationConverterTest.java
@@ -19,7 +19,7 @@ import java.util.Collection;
 import static com.sap.cloud.security.config.Service.XSUAA;
 import static org.junit.jupiter.api.Assertions.*;
 
-class XsuaaTokenAuthenticationConverterTest {
+class XsuaaTokenAuthorizationConverterTest {
 	String xsAppName = "my-app-name!400";
 	JwtGenerator jwtGenerator = JwtGenerator.getInstance(XSUAA, "theClientId").withAppId(xsAppName);
 	XsuaaTokenAuthorizationConverter cut = new XsuaaTokenAuthorizationConverter(xsAppName);
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 0279cc75..90129404 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
@@ -14,6 +14,7 @@ import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 
+import com.sap.cloud.security.xsuaa.extractor.LocalAuthoritiesExtractor;
 import org.junit.Before;
 import org.junit.Test;
 import org.springframework.security.authentication.AbstractAuthenticationToken;
@@ -106,6 +107,21 @@ public class TokenAuthenticationConverterTest {
 		assertThat(authenticationToken.getAuthorities(), hasItem(new SimpleGrantedAuthority("Read")));
 	}
 
+	@Test
+	public void checkFollowingInstanceScope() {
+		String scopeWithClientId = "7cf2e319-3a7d-4f99-8207-afdc8e8e6d64!b123|trustedclientid!b333.API_OVERVIEW";
+
+		Jwt jwt = new JwtGenerator("sb-7cf2e319-3a7d-4f99-8207-afdc8e8e6d64!b123|trustedclientid!b333")
+				.addScopes(xsAppName + "." + scopeAdmin, scopeRead, scopeWithClientId)
+				.getToken();
+		TokenAuthenticationConverter converter = new TokenAuthenticationConverter(
+				new MyFollowingInstanceAuthoritiesExtractor());
+
+		assertThat(converter.convert(jwt).getAuthorities().size(), is(1));
+		assertThat(converter.convert(jwt).getAuthorities(), hasItem(new SimpleGrantedAuthority("API_OVERVIEW")));
+
+	}
+
 	private static class MyAuthoritiesExtractor implements AuthoritiesExtractor {
 		private String[] xsUserAttributes;
 		private AuthoritiesExtractor authoritiesExtractor;
@@ -141,4 +157,18 @@ public class TokenAuthenticationConverterTest {
 		}
 
 	}
+
+	private static class MyFollowingInstanceAuthoritiesExtractor implements AuthoritiesExtractor {
+
+		@Override
+		public Collection<GrantedAuthority> getAuthorities(XsuaaToken token) {
+			String appId = "";
+			if (token.getClientId().startsWith("sb-")) {
+				appId = token.getClientId().replaceFirst("sb-", "");
+			}
+			AuthoritiesExtractor authoritiesExtractor = new LocalAuthoritiesExtractor(appId);
+			return authoritiesExtractor.getAuthorities(token);
+		}
+
+	}
 }
diff --git a/token-client/src/test/java/com/sap/cloud/security/mtls/SSLContextFactoryTest.java b/token-client/src/test/java/com/sap/cloud/security/mtls/SSLContextFactoryTest.java
index 59b13302..b141e868 100644
--- a/token-client/src/test/java/com/sap/cloud/security/mtls/SSLContextFactoryTest.java
+++ b/token-client/src/test/java/com/sap/cloud/security/mtls/SSLContextFactoryTest.java
@@ -14,6 +14,8 @@ import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
 
+import com.sap.cloud.security.config.ClientCertificate;
+import com.sap.cloud.security.config.ClientCredentials;
 import com.sap.cloud.security.mtls.SSLContextFactory;
 import org.apache.commons.io.IOUtils;
 import org.junit.Before;
@@ -43,6 +45,18 @@ public class SSLContextFactoryTest {
 		assertThatThrownBy(() -> {
 			cut.create(certificates, null);
 		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("rsaPrivateKey");
+
+		assertThatThrownBy(() -> {
+			cut.create(null);
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientIdentity");
+
+		assertThatThrownBy(() -> {
+			cut.create(new ClientCredentials("clientId", "clientSecret"));
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientIdentity.getCertificate()");
+
+		assertThatThrownBy(() -> {
+			cut.create(new ClientCertificate("certificate", null, null));
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientIdentity.getKey()");
 	}
 
 	@Test
@@ -54,7 +68,23 @@ public class SSLContextFactoryTest {
 		assertThat(cut.create(certificates, rsaPrivateKey), is(notNullValue()));
 	}
 
-	public String readFromFile(String file) throws IOException {
+	@Test
+	public void createKeyStore() throws GeneralSecurityException, IOException {
+		assertThat(cut.createKeyStore(new ClientCertificate(certificates, rsaPrivateKey, null)), is(notNullValue()));
+	}
+
+	@Test
+	public void createKeyStore_throwsOnNullValues() {
+		assertThatThrownBy(() -> {
+			cut.createKeyStore(new ClientCredentials("clientId", "clientSecret"));
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientIdentity.getCertificate()");
+
+		assertThatThrownBy(() -> {
+			cut.createKeyStore(new ClientCertificate("certificate", null, null));
+		}).isInstanceOf(IllegalArgumentException.class).hasMessageStartingWith("clientIdentity.getKey()");
+	}
+
+	private String readFromFile(String file) throws IOException {
 		return IOUtils.resourceToString(file, StandardCharsets.UTF_8);
 	}