Repository: SAP/cloud-security-xsuaa-integration
Pull request #354 (2020-08-18 12:19:51)

Improve SAPOfflineTokenServicesCloud compatibility
This PR improves the compatibility of the `SAPOfflineTokenServicesCloud` with the old implementation. 


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bb35c62..38bd28f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 All notable changes to this project will be documented in this file.
 
+## 2.7.6
+- [java-security] Improve compatibility of `SAPOfflineTokenServicesCloud`
+  - There was incompatibility in the implementation of `SAPOfflineTokenServicesCloud` that caused the `remoteUser` of the `HttpServletRequest` to always return the client id of the XSUAA service binding. This was changed so that it now works like in the old implementation. This means that the `remoteUser` now returns either the `user_name` claim of the token for user tokens or the value of the client id `cid` claim of the token for all other tokens (e.g. client tokens).
+
 ## 2.7.5
 - [java-api] `AcessToken` provides  
   - `getAttributeFromClaimAsString(String claimName, String attributeName)` to access for example `ext_attr` values such as `subaccountid`
diff --git a/java-security/Migration_SpringSecurityProjects.md b/java-security/Migration_SpringSecurityProjects.md
index d4f82606..70eeb1dc 100644
--- a/java-security/Migration_SpringSecurityProjects.md
+++ b/java-security/Migration_SpringSecurityProjects.md
@@ -54,15 +54,29 @@ First make sure you have the following dependencies defined in your pom.xml:
 </dependency>
 ```
 
-Now you are ready to **remove** the **`java-container-security`** client library by deleting the following dependencies from the pom.xml:
 
-groupId (deprecated) | artifactId (deprecated) 
---- | --- 
-com.sap.xs2.security | java-container-security
-com.sap.xs2.security | api
-com.sap.cloud.security.xssec | api 
-com.sap.cloud.security.xsuaa | java-container-security-api
-com.sap.cloud.security.xsuaa | java-container-security
+Now you are ready to **remove** the **`java-container-security`** client library by deleting the following lines from the pom.xml:
+```xml
+<dependency>
+  <groupId>com.sap.xs2.security</groupId>
+  <artifactId>java-container-security</artifactId>
+</dependency>
+<dependency>
+  <groupId>com.sap.xs2.security</groupId>
+  <artifactId>java-container-security-api</artifactId>
+</dependency>
+```
+Or
+```xml
+<dependency>
+  <groupId>com.sap.cloud.security.xsuaa</groupId>
+  <artifactId>java-container-security</artifactId>
+</dependency>
+<dependency>
+  <groupId>com.sap.cloud.security.xsuaa</groupId>
+  <artifactId>api</artifactId>
+</dependency>
+```
 
 Make sure that you do not refer to any other sap security library with group-id `com.sap.security` or `com.sap.security.nw.sso.*`. 
 
@@ -108,7 +122,7 @@ Or
 ```java
 ...
 .authorizeRequests()
-	.antMatchers(POST, "/api/v1/ads/**").access("#oauth2.hasScopeMatching('Update')") //instead of '${xs.appname}.Update'
+	.antMatchers(POST, "/api/v1/ads/**").access(#oauth2.hasScopeMatching('Update')) //instead of '${xs.appname}.Update'
 ```
 
 
diff --git a/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java b/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java
index d99ea166..ce6e76b6 100644
--- a/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java
+++ b/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloud.java
@@ -11,6 +11,7 @@ import com.sap.cloud.security.xsuaa.Assertions;
 import com.sap.cloud.security.xsuaa.client.SpringOAuth2TokenKeyService;
 import com.sap.cloud.security.xsuaa.client.SpringOidcConfigurationService;
 import org.springframework.beans.factory.InitializingBean;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.core.GrantedAuthority;
@@ -25,7 +26,11 @@ import org.springframework.web.client.RestOperations;
 import org.springframework.web.client.RestTemplate;
 
 import javax.annotation.Nonnull;
-import java.util.*;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletRequest;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -46,10 +51,17 @@ import java.util.stream.Collectors;
  * </dependency>
  * }
  * </pre>
- * 
+ *
  * By default it used Apache Rest Client for communicating with the OAuth2
  * Server.<br>
  *
+ * <p>
+ * When used in conjunction with Java Http Servlets, the
+ * {@link HttpServletRequest#getRemoteUser()} will be filled with either the
+ * <code>user_name</code> claim of the token or the client id (<code>cid</code>)
+ * if it is not an user token.
+ * </p>
+ *
  * Spring Security framework initializes the
  * {@link org.springframework.security.core.context.SecurityContext} with the
  * {@code OAuth2Authentication} which is provided as part of
@@ -112,7 +124,6 @@ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices
 		this(serviceConfiguration, JwtValidatorBuilder.getInstance(serviceConfiguration)
 				.withOAuth2TokenKeyService(new SpringOAuth2TokenKeyService(restOperations))
 				.withOidcConfigurationService(new SpringOidcConfigurationService(restOperations)));
-
 	}
 
 	SAPOfflineTokenServicesCloud(OAuth2ServiceConfiguration serviceConfiguration,
@@ -155,20 +166,34 @@ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices
 			throw new InvalidTokenException(validationResult.getErrorDescription());
 		}
 		SecurityContext.setToken(token);
-
-		return getOAuth2Authentication(serviceConfiguration.getClientId(), getScopes(token));
+		// remoteUser support: NGPBUG-125268
+		return createOAuth2Authentication(token.getClaimAsString(TokenClaims.XSUAA.CLIENT_ID), getScopes(token), token);
 	}
 
-	static OAuth2Authentication getOAuth2Authentication(String clientId, Set<String> scopes) {
-		Authentication userAuthentication = null; // TODO no SAPUserDetails support. Using spring alternative?
-
+	static OAuth2Authentication createOAuth2Authentication(String clientId, Set<String> scopes, Token token) {
+		Authentication userAuthentication = getUserAuthentication(token, scopes);
 		final AuthorizationRequest authorizationRequest = new AuthorizationRequest(clientId, scopes);
-		authorizationRequest.setAuthorities(getAuthorities(scopes));
+		authorizationRequest.setAuthorities(createAuthorities(scopes));
 		authorizationRequest.setApproved(true);
-
 		return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), userAuthentication);
 	}
 
+	@Nullable
+	private static UserAuthenticationToken getUserAuthentication(Token token, Set<String> scopes) {
+		GrantType grantType = null;
+		if (token instanceof AccessToken) {
+			grantType = ((AccessToken) token).getGrantType();
+		}
+		if (grantType == GrantType.CLIENT_CREDENTIALS || grantType == GrantType.CLIENT_X509) {
+			return null;
+		}
+		String username = token.getClaimAsString(TokenClaims.USER_NAME);
+		if (username == null) {
+			return null;
+		}
+		return new UserAuthenticationToken(token, scopes);
+	}
+
 	private Set<String> getScopes(Token token) {
 		Set<String> scopes = token instanceof AccessToken
 				? ((AccessToken) token).getScopes()
@@ -204,7 +229,7 @@ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices
 		return this;
 	}
 
-	private static Set<GrantedAuthority> getAuthorities(Collection<String> scopes) {
+	private static Set<GrantedAuthority> createAuthorities(Collection<String> scopes) {
 		return scopes.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());
 	}
 
@@ -224,4 +249,30 @@ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices
 			throw new InvalidTokenException(e.getMessage());
 		}
 	}
+
+	private static class UserAuthenticationToken extends AbstractAuthenticationToken {
+		private final String username;
+
+		public UserAuthenticationToken(Token token, Set<String> scopes) {
+			super(SAPOfflineTokenServicesCloud.createAuthorities(scopes));
+			this.username = token.getClaimAsString(TokenClaims.USER_NAME);
+			setAuthenticated(true);
+			setDetails(token);
+		}
+
+		@Override
+		public String getName() {
+			return username;
+		}
+
+		@Override
+		public Object getCredentials() {
+			return "N/A";
+		}
+
+		@Override
+		public Object getPrincipal() {
+			return username;
+		}
+	}
 }
diff --git a/spring-xsuaa/Migration_JavaContainerSecurityProjects.md b/spring-xsuaa/Migration_JavaContainerSecurityProjects.md
index 8128ed25..6fd9a7d0 100644
--- a/spring-xsuaa/Migration_JavaContainerSecurityProjects.md
+++ b/spring-xsuaa/Migration_JavaContainerSecurityProjects.md
@@ -39,20 +39,31 @@ which shows what had to be changed to migrate our open-SAP course application fr
 To use the new [spring-xsuaa](/spring-xsuaa) client library the dependencies declared in maven `pom.xml` need to be changed.
 See the [docs](/spring-xsuaa#configuration) on what to add to your `pom.xml`.
 
-Now you are ready to **remove** the **`java-container-security`** client library by deleting the following dependencies from the pom.xml:
-
-groupId (deprecated) | artifactId (deprecated) 
---- | --- 
-com.sap.xs2.security | java-container-security
-com.sap.xs2.security | api
-com.sap.cloud.security.xssec | api 
-com.sap.cloud.security.xsuaa | java-container-security-api
-com.sap.cloud.security.xsuaa | java-container-security
-com.sap.cloud.security.xsuaa | api
-
-> Note: The dependency `com.sap.cloud.security.xsuaa:api` should be removed as well, as `spring-xsuaa` provides it already as transitive dependency.
+After you have added to new dependencies you are ready to **remove** the **`java-container-security`** client library by
+deleting the following lines from the pom.xml:
+```xml
+<dependency>
+  <groupId>com.sap.xs2.security</groupId>
+  <artifactId>java-container-security</artifactId>
+</dependency>
+<dependency>
+  <groupId>com.sap.xs2.security</groupId>
+  <artifactId>java-container-security-api</artifactId>
+</dependency>
+```
+Or
+```xml
+<dependency>
+  <groupId>com.sap.cloud.security.xsuaa</groupId>
+  <artifactId>java-container-security</artifactId>
+</dependency>
+<dependency>
+  <groupId>com.sap.cloud.security.xsuaa</groupId>
+  <artifactId>api</artifactId>
+</dependency>
+```
 
-Furthermore, make sure that you do not refer to any other sap security library with groupId `com.sap.security` or `com.sap.security.nw.sso.*`.
+Make sure that you do not refer to any other sap security library with group-id `com.sap.security` or `com.sap.security.nw.sso.*`. 
 
 ## Configuration changes
 After the dependencies have been changed, the spring security configuration needs some adjustments as well.
diff --git a/spring-xsuaa/README.md b/spring-xsuaa/README.md
index bf16f10c..862f929c 100644
--- a/spring-xsuaa/README.md
+++ b/spring-xsuaa/README.md
@@ -199,12 +199,6 @@ public Map<String, String> message() {
 }
 ```
 
-### [Optional] Audit Logging
-In case you have implemented a central Exception Handler as described with [Baeldung Tutorial: Error Handling for REST with Spring](https://www.baeldung.com/exception-handling-for-rest-with-spring) you may want to emit logs to the audit log service in case of `AccessDeniedException`s.
-
-Alternativly there are also various options provided with `Spring.io`. For example, you can integrate SAP audit log service with Spring Boot Actuator audit framework as described [here](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-auditing).
-
-
 ## Samples
 - [spring-security-xsuaa-usage](/samples/spring-security-xsuaa-usage) 
 


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java
index 9f102c24..caee520a 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SAPOfflineTokenServicesCloudTest.java
@@ -12,14 +12,17 @@ import org.apache.commons.io.IOUtils;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mockito;
+import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
 import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.OAuth2Request;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.Collection;
+import java.util.List;
 import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -29,13 +32,16 @@ import static org.mockito.Mockito.*;
 public class SAPOfflineTokenServicesCloudTest {
 
 	private SAPOfflineTokenServicesCloud cut;
-	private String xsuaaToken;
-	private String iasToken;
 	private JwtValidatorBuilder jwtValidatorBuilderSpy;
 
+	private final String xsuaaToken;
+	private final String iasToken;
+	private final String userToken;
+
 	public SAPOfflineTokenServicesCloudTest() throws IOException {
 		xsuaaToken = IOUtils.resourceToString("/xsuaaCCAccessTokenRSA256.txt", StandardCharsets.UTF_8);
 		iasToken = IOUtils.resourceToString("/iasOidcTokenRSA256.txt", StandardCharsets.UTF_8);
+		userToken = IOUtils.resourceToString("/xsuaaUserInfoAdapterToken.txt", StandardCharsets.UTF_8);
 	}
 
 	@Before
@@ -56,22 +62,34 @@ public class SAPOfflineTokenServicesCloudTest {
 	}
 
 	@Test
-	public void loadAuthentication() {
+	public void loadAuthentication_setsOAuth2Request() {
 		cut.afterPropertiesSet();
 		OAuth2Authentication authentication = cut.loadAuthentication(xsuaaToken);
+		OAuth2Request oAuth2Request = authentication.getOAuth2Request();
 
-		assertThat(authentication.isAuthenticated()).isTrue();
-		assertThat(authentication.getOAuth2Request()).isNotNull();
-		assertThat(authentication.getOAuth2Request().getScope()).containsExactlyInAnyOrder("ROLE_SERVICEBROKER",
-				"uaa.resource");
-		Collection<String> authorities = authentication.getOAuth2Request().getAuthorities().stream()
-				.map(GrantedAuthority::getAuthority).collect(
-						Collectors.toList());
+		assertThat(oAuth2Request).isNotNull();
+		assertThat(oAuth2Request.getScope()).containsExactlyInAnyOrder("ROLE_SERVICEBROKER", "uaa.resource");
+		Collection<String> authorities = oAuth2Request.getAuthorities().stream()
+				.map(GrantedAuthority::getAuthority).collect(Collectors.toList());
 		assertThat(authorities).containsExactlyInAnyOrder("ROLE_SERVICEBROKER", "uaa.resource");
-		assertThat(authentication.getAuthorities()).contains(new SimpleGrantedAuthority("uaa.resource"));
+		assertThat(authentication.getAuthorities()).contains
+				(new SimpleGrantedAuthority("uaa.resource"), new SimpleGrantedAuthority("ROLE_SERVICEBROKER"));
 		assertThat(SecurityContext.getToken().getTokenValue()).isEqualTo(xsuaaToken);
 	}
 
+	@Test
+	public void loadAuthentication_userToken() {
+		cut.afterPropertiesSet();
+		OAuth2Authentication oAuth2Authentication = cut.loadAuthentication(userToken);
+		Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
+
+		assertThat(oAuth2Authentication.isAuthenticated()).isTrue();
+		assertThat(getAuthorities(oAuth2Authentication))
+				.containsExactlyInAnyOrder("testApp.localScope", "testScope", "openid");
+		assertThat(userAuthentication).isNotNull();
+		assertThat(userAuthentication.getPrincipal()).isEqualTo("TestUser");
+	}
+
 	@Test
 	public void loadAuthentication_ias() {
 		OAuth2ServiceConfiguration configuration = OAuth2ServiceConfigurationBuilder
@@ -85,23 +103,17 @@ public class SAPOfflineTokenServicesCloudTest {
 		OAuth2Authentication authentication = cut.loadAuthentication(iasToken);
 
 		assertThat(authentication.isAuthenticated()).isTrue();
+		assertThat(authentication.getUserAuthentication()).isNull();
 	}
 
 	@Test
-	public void loadAuthenticationWithLocalScopes() throws IOException {
-		xsuaaToken = IOUtils.resourceToString("/xsuaaUserInfoAdapterToken.txt", StandardCharsets.UTF_8);
-
+	public void loadAuthenticationWithLocalScopes() {
 		cut.afterPropertiesSet();
-
-		OAuth2Authentication authentication = cut.loadAuthentication(xsuaaToken);
-		assertThat(authentication.getOAuth2Request().getScope()).containsExactlyInAnyOrder("testApp.localScope",
-				"openid", "testScope");
-
 		cut.setLocalScopeAsAuthorities(true);
-		authentication = cut.loadAuthentication(xsuaaToken);
-		assertThat(authentication.getOAuth2Request().getScope()).containsExactly("localScope");
-		assertThat(authentication.getAuthorities().size()).isEqualTo(1);
-		assertThat(authentication.getAuthorities()).contains(new SimpleGrantedAuthority("localScope"));
+		OAuth2Authentication oAuth2Authentication = cut.loadAuthentication(userToken);
+
+		assertThat(oAuth2Authentication.isAuthenticated()).isTrue();
+		assertThat(getAuthorities(cut.loadAuthentication(userToken))).containsExactlyInAnyOrder("localScope");
 	}
 
 	@Test
@@ -172,4 +184,11 @@ public class SAPOfflineTokenServicesCloudTest {
 		Mockito.verify(jwtValidatorBuilderSpy, times(1)).build();
 	}
 
+	private List<String> getAuthorities(OAuth2Authentication oAuth2Authentication) {
+		return oAuth2Authentication.getAuthorities()
+				.stream()
+				.map(GrantedAuthority::getAuthority)
+				.collect(Collectors.toList());
+	}
+
 }
\ No newline at end of file
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextTest.java
index d6766ea5..441a84eb 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextTest.java
@@ -111,8 +111,8 @@ public class SpringSecurityContextTest {
 
 	private static void setToken(Token token, Set<String> scopes) {
 		SecurityContext context = new SecurityContextImpl();
-		OAuth2Authentication authentication = SAPOfflineTokenServicesCloud.getOAuth2Authentication(
-				"clientId", scopes);
+		OAuth2Authentication authentication = SAPOfflineTokenServicesCloud.createOAuth2Authentication(
+				"clientId", scopes, token);
 
 		HttpServletRequest request = mock(HttpServletRequest.class);
 		when(request.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE)).thenReturn(token.getTokenValue());