Repository: SAP/cloud-security-xsuaa-integration
Pull request #232 (2020-02-21 14:51:50)

SpringSecurityContext for access within asynch threads


Patch:
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 ff57c45b..d825cd39 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
@@ -3,7 +3,12 @@ package com.sap.cloud.security.adapter.spring;
 import com.sap.cloud.security.config.Environments;
 import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
 import com.sap.cloud.security.config.cf.CFConstants;
-import com.sap.cloud.security.token.*;
+import com.sap.cloud.security.token.AccessToken;
+import com.sap.cloud.security.token.ScopeConverter;
+import com.sap.cloud.security.token.SecurityContext;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.XsuaaScopeConverter;
+import com.sap.cloud.security.token.XsuaaToken;
 import com.sap.cloud.security.token.validation.ValidationResult;
 import com.sap.cloud.security.token.validation.Validator;
 import com.sap.cloud.security.token.validation.validators.JwtValidatorBuilder;
@@ -12,6 +17,7 @@ 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.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.oauth2.common.OAuth2AccessToken;
 import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
 import org.springframework.security.oauth2.provider.AuthorizationRequest;
@@ -28,7 +34,7 @@ import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
- * This constructor requires a dependency to Spring oauth.
+ * This constructor requires a dependency to Spring-security oauth, which will be deprecated soon.
  *
  * <pre>
  * {@code
@@ -45,8 +51,20 @@ import java.util.stream.Collectors;
  * }
  * </pre>
  * 
- * By default it used Apache Rest Client for communicating with the OAuth2
- * Server.
+ * By default it used Apache Rest Client for communicating with the OAuth2 Server.<br>
+ *
+ * Spring Security framework initializes the {@link org.springframework.security.core.context.SecurityContext}
+ * with the {@code OAuth2Authentication} which is provided as part of {@link #loadAuthentication} method. <br>
+ * This gives you the following options:
+ * <li>
+ *     <ul>All Spring security features are supported that uses
+ *     {@link org.springframework.security.core.context.SecurityContext#getAuthentication()}</ul>
+ *     <ul>You can access the {@code Authentication} via {@link SecurityContextHolder#getContext()}
+ *     also within asynchronous threads.</ul>
+ *     <ul>You can access the {@code Token} via {@link SpringSecurityContext#getToken()}
+ *     also within asynchronous threads.</ul>
+ * </li>
+ *
  */
 public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices, InitializingBean {
 
@@ -114,24 +132,33 @@ public class SAPOfflineTokenServicesCloud implements ResourceServerTokenServices
 			throws AuthenticationException, InvalidTokenException {
 		Token token = checkAndCreateToken(accessToken);
 
+		ValidationResult validationResult = tokenValidator.validate(token);
+
+		if (validationResult.isErroneous()) {
+			throw new InvalidTokenException(validationResult.getErrorDescription());
+		}
+
+		return getOAuth2Authentication(token, serviceConfiguration.getClientId(), getScopes(token));
+	}
+
+	static OAuth2Authentication getOAuth2Authentication(Token token, String clientId, Set<String> scopes) {
+
+		AuthorizationRequest authorizationRequest = new AuthorizationRequest(new HashMap<>(), null,
+				clientId, scopes.stream().collect(Collectors.toSet()), new HashSet<>(),
+				null,
+				true, "", "", null);
+		SecurityContext.setToken(token);
+		return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), null);
+	}
+
+	private Set<String> getScopes(Token token) {
 		Set<String> scopes = token instanceof AccessToken
 				? ((AccessToken) token).getScopes()
 				: Collections.emptySet();
 		if (useLocalScopeAsAuthorities) {
 			scopes = xsuaaScopeConverter.convert(scopes);
 		}
-		ValidationResult validationResult = tokenValidator.validate(token);
-
-		if (validationResult.isValid()) {
-			AuthorizationRequest authorizationRequest = new AuthorizationRequest(new HashMap<>(), null,
-					serviceConfiguration.getClientId(), scopes.stream().collect(Collectors.toSet()), new HashSet<>(),
-					null,
-					true, "", "", null);
-			SecurityContext.setToken(token);
-			return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), null);
-		} else {
-			throw new InvalidTokenException(validationResult.getErrorDescription());
-		}
+		return scopes;
 	}
 
 	@Override
diff --git a/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SpringSecurityContext.java b/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SpringSecurityContext.java
new file mode 100644
index 00000000..f2295a68
--- /dev/null
+++ b/java-security/src/main/java/com/sap/cloud/security/adapter/spring/SpringSecurityContext.java
@@ -0,0 +1,70 @@
+package com.sap.cloud.security.adapter.spring;
+
+import com.sap.cloud.security.token.AccessToken;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.XsuaaToken;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
+
+import javax.annotation.Nullable;
+import java.util.Objects;
+
+/**
+ * This is an alternative way of accessing jwt tokens of type {@link Token} or {@link AccessToken}
+ * in context of an application using spring-security-oauth2.
+ *
+ * It uses the {@link SecurityContextHolder} to access Spring's {@link SecurityContext}
+ *  and can therefore used also in context of asynchronous threads.
+ */
+public class SpringSecurityContext {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SpringSecurityContext.class);
+
+    private SpringSecurityContext() {
+    }
+
+    /**
+     * Returns the token using {@link SecurityContextHolder}.
+     *
+     *
+     * @return the token or <code>null</code> if {@link SecurityContext} is empty
+     * or does not contain a token of this type.
+     */
+    @Nullable
+    public static Token getToken() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if(Objects.nonNull(authentication) && authentication.isAuthenticated() &&
+                authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
+            OAuth2AuthenticationDetails authDetails = (OAuth2AuthenticationDetails) authentication.getDetails();
+            String tokenValue = authDetails.getTokenValue();
+            // TODO IAS Support
+            return new XsuaaToken(tokenValue);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the token using {@link SecurityContextHolder}.
+     *
+     *
+     * @return the token or <code>null</code> if {@link SecurityContext} is empty
+     * or does not contain a token of this type.
+     */
+    @Nullable
+    public static AccessToken getAccessToken() {
+        Token token = getToken();
+        return token instanceof AccessToken ? (AccessToken) token : null;
+    }
+
+    /**
+     * Clears the context value from the current thread.
+     */
+    public static void clear() {
+        SecurityContextHolder.clearContext();
+    }
+
+}
+


Test patch:
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
new file mode 100644
index 00000000..34c8d436
--- /dev/null
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextTest.java
@@ -0,0 +1,113 @@
+package com.sap.cloud.security.adapter.spring;
+
+import com.sap.cloud.security.token.AccessToken;
+import com.sap.cloud.security.token.SapIdToken;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.token.XsuaaToken;
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SpringSecurityContextTest {
+    AccessToken token;
+    SapIdToken sapIdToken;
+    private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+    @Before
+    public void setUp() throws IOException {
+        token = new XsuaaToken(IOUtils.resourceToString("/xsuaaUserAccessTokenRSA256.txt", UTF_8));
+        sapIdToken = new SapIdToken(IOUtils.resourceToString("/iasOidcTokenRSA256.txt", UTF_8));
+        SpringSecurityContext.clear();
+    }
+
+    @Test
+    public void getToken_fromEmptySecurityContext_isNull() {
+        Token token = SpringSecurityContext.getToken();
+        assertThat(token).isNull();
+
+        token = SpringSecurityContext.getAccessToken();
+        assertThat(token).isNull();
+    }
+
+    @Test
+    public void getToken() {
+        setToken(sapIdToken);
+        assertThat(SpringSecurityContext.getToken()).isEqualTo(sapIdToken);
+    }
+
+    @Test
+    public void getAccessToken() {
+        setToken(token);
+        assertThat(SpringSecurityContext.getAccessToken()).isEqualTo(token);
+    }
+
+    @Test
+    @Ignore // TODO IAS Support
+    public void getAccessTokenReturnsNullIfTokenDoesNotImplementInterface() {
+        setToken(sapIdToken);
+        assertThat(SpringSecurityContext.getAccessToken()).isNull();
+    }
+
+    @Test
+    public void clear_removesToken() {
+        setToken(token);
+        SpringSecurityContext.clear();
+
+        assertThat(SpringSecurityContext.getToken()).isNull();
+    }
+
+    @Test
+    public void tokenNotAvailableInDifferentThread() throws ExecutionException, InterruptedException {
+        setToken(token);
+
+        Future<Token> tokenInOtherThread = executorService.submit(() -> SpringSecurityContext.getToken());
+
+        assertThat(tokenInOtherThread.get()).isNull();
+    }
+
+    @Test
+    public void clearingTokenInDifferentThreadDoesNotAffectMainThread()
+            throws ExecutionException, InterruptedException {
+        setToken(token);
+
+        executorService.submit(() -> SpringSecurityContext.clear()).get(); // run and await other thread
+
+        assertThat(SpringSecurityContext.getToken()).isEqualTo(token);
+    }
+
+
+
+    private static void setToken(Token token) {
+        SecurityContext context = new SecurityContextImpl();
+        OAuth2Authentication authentication = SAPOfflineTokenServicesCloud.getOAuth2Authentication(token,
+                "clientId", Collections.EMPTY_SET);
+
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE)).thenReturn(token.getTokenValue());
+
+        authentication.setDetails(new OAuth2AuthenticationDetails(request));
+        context.setAuthentication(authentication);
+        SecurityContextHolder.clearContext();
+        SecurityContextHolder.setContext(context);
+        assertThat(SecurityContextHolder.getContext()).isEqualTo(context);
+    }
+}
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/MockTokenBuilder.java b/java-security/src/test/java/com/sap/cloud/security/token/MockTokenBuilder.java
index ae06ab1c..eadb48e0 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/MockTokenBuilder.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/MockTokenBuilder.java
@@ -3,7 +3,6 @@ package com.sap.cloud.security.token;
 import org.mockito.Mockito;
 
 import java.time.Instant;
-import java.util.Arrays;
 import java.util.GregorianCalendar;
 
 import static org.mockito.Mockito.when;
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/SecurityContextTest.java b/java-security/src/test/java/com/sap/cloud/security/token/SecurityContextTest.java
index 94020633..a6ad6ac3 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/SecurityContextTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/SecurityContextTest.java
@@ -40,18 +40,15 @@ public class SecurityContextTest {
 	@Test
 	public void setTokenAndGet() {
 		SecurityContext.setToken(TOKEN);
-		Token token = SecurityContext.getToken();
-
-		assertThat(token).isEqualTo(TOKEN);
+		assertThat(SecurityContext.getToken()).isEqualTo(TOKEN);
 	}
 
 	@Test
 	public void clear_removesToken() {
 		SecurityContext.setToken(TOKEN);
 		SecurityContext.clearToken();
-		Token token = SecurityContext.getToken();
 
-		assertThat(token).isNull();
+		assertThat(SecurityContext.getToken()).isNull();
 	}
 
 	@Test
@@ -74,14 +71,13 @@ public class SecurityContextTest {
 	}
 
 	@Test
-	public void getAccessTokenReturnsNullIfTokenDoesNotImplementInterface()
-			throws ExecutionException, InterruptedException, IOException {
+	public void getAccessTokenReturnsNullIfTokenDoesNotImplementInterface() {
 		SecurityContext.setToken(TOKEN);
 		assertThat(SecurityContext.getAccessToken()).isNull();
 	}
 
 	@Test
-	public void getAccessTokenReturns() throws ExecutionException, InterruptedException, IOException {
+	public void getAccessTokenReturns() throws IOException {
 		AccessToken accessToken = new XsuaaToken(IOUtils.resourceToString("/xsuaaUserAccessTokenRSA256.txt", UTF_8));
 		SecurityContext.setToken(accessToken);
 		assertThat(SecurityContext.getAccessToken()).isEqualTo(accessToken);