Repository: SAP/cloud-security-xsuaa-integration
Pull request #482 (2021-03-05 16:06:04)

java-security offers adapter to read from spring-xsuaa SecurityContext


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 23f64263..d3835c92 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,13 @@
 
 All notable changes to this project will be documented in this file.
 
-## 2.8.8 and 0.1.1 [BETA]
+## 2.8.8
+- [java-security] supports with ``SpringSecurityContext`` a way to read tokens from Spring's `SecurityContextHolder`, in case a token was set by the application using one of these client-libraries:
+  -  `org.springframework.security.oauth:spring-security-oauth2`
+  -  `com.sap.cloud.security.xsuaa:spring-xsuaa`
+  -  `com.sap.cloud.security:spring-security`
+
+## 2.8.7 and 0.1.1 [BETA]
 - [xsuaa-spring-boot-starter] and [resourceserver-security-spring-boot-starter (BETA)]
   - spring.core.version 5.3.3 --> 5.3.4
   - spring.boot.version 2.4.2 --> 2.4.3
diff --git a/java-security/pom.xml b/java-security/pom.xml
index 9bbe7745..022a9b70 100644
--- a/java-security/pom.xml
+++ b/java-security/pom.xml
@@ -114,6 +114,25 @@
 			<version>1.1.1</version>
 			<scope>test</scope>
 		</dependency>
+		<dependency>
+			<groupId>com.sap.cloud.security.xsuaa</groupId>
+			<artifactId>spring-xsuaa</artifactId>
+			<version>${project.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.security</groupId>
+			<artifactId>spring-security-oauth2-jose</artifactId>
+			<version>${spring.security.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.security</groupId>
+			<artifactId>spring-security-oauth2-resource-server</artifactId>
+			<version>${spring.security.version}</version>
+			<scope>test</scope>
+		</dependency>
+
 	</dependencies>
 
 	<build>
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
index 53d3a101..505ecaaf 100644
--- 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
@@ -1,15 +1,18 @@
 package com.sap.cloud.security.adapter.spring;
 
 import com.sap.cloud.security.token.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 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.Nonnull;
 import javax.annotation.Nullable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.Objects;
 
@@ -21,36 +24,87 @@ import java.util.Objects;
  * It uses the {@link SecurityContextHolder} to access Spring's
  * {@link SecurityContext} and can therefore used also in context of
  * asynchronous threads.
+ *
+ * Use this class in case your application sets Spring's security context via
+ * one of these libraries: <br>
+ * <ol>
+ * <li>@code{org.springframework.security.oauth:spring-security-oauth2} or</li>
+ * <li>@code{com.sap.cloud.security.xsuaa:spring-xsuaa} client library.</li>
+ * </ol>
  */
 public class SpringSecurityContext {
 
+	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();
-			AbstractToken xsuaaToken = new XsuaaTokenWithGrantedAuthorities(tokenValue,
-					authentication.getAuthorities());
-			if (xsuaaToken.isXsuaaToken()) {
-				return xsuaaToken;
+		if (Objects.nonNull(authentication) && authentication.isAuthenticated()) {
+			try {
+				if (authentication.getDetails() != null && authentication.getDetails().getClass()
+						.getName().equals("org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails")) {
+					LOGGER.debug("Try to fetch token from deprecated Springs Auth2 client library.");
+					return getTokenFromDeprecatedLib(authentication);
+				} else if (authentication.getPrincipal() == null) {
+					return null; // no token available
+				}
+				if (authentication.getPrincipal().getClass().getName()
+						.startsWith("com.sap.cloud.security.xsuaa.token.")) {
+					//startsWith("org.springframework.security.oauth2.core.oidc.user.OidcIdToken
+					LOGGER.debug("Try to fetch token from SecurityContextHolder.getPrincipal() of type {}.",
+							authentication.getPrincipal().getClass().getName());
+					return getSpringXsuaaToken(authentication);
+				} else if (authentication.getPrincipal().getClass().getName()
+						.startsWith("com.sap.cloud.security.token.")) {
+					LOGGER.debug("Try to fetch token from SecurityContextHolder.getPrincipal() of type {}.",
+							authentication.getPrincipal().getClass().getName());
+					return (Token) authentication.getPrincipal();
+				}
+			} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
+				LOGGER.error("Does not yet support (Tokens of class: {})", authentication.getPrincipal().getClass());
 			}
-			return new SapIdToken(tokenValue);
+		} else {
+			LOGGER.debug("Spring SecurityContextHolder does not contain a token which was authenticated ({})",
+					authentication);
 		}
 		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
+	static Token getTokenFromDeprecatedLib(Authentication authentication)
+			throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+		Method getTokenValue = authentication.getDetails().getClass().getMethod("getTokenValue");
+		String encodedToken = (String) getTokenValue.invoke(authentication.getDetails());
+		AbstractToken xsuaaToken = new XsuaaTokenWithGrantedAuthorities(encodedToken,
+				authentication.getAuthorities());
+		if (xsuaaToken.isXsuaaToken()) {
+			return xsuaaToken;
+		}
+		return new SapIdToken(encodedToken);
+	}
+
+	static Token getSpringXsuaaToken(Authentication authentication)
+			throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
+		Method getAppToken = authentication.getPrincipal().getClass().getMethod("getAppToken");
+		String encodedToken = (String) getAppToken.invoke(authentication.getPrincipal());
+		return Token.create(encodedToken);
+	}
+
 	/**
 	 * Returns the token using {@link SecurityContextHolder}.
 	 *
@@ -91,5 +145,4 @@ public class SpringSecurityContext {
 			return authorities.contains(new SimpleGrantedAuthority(scope));
 		}
 	}
-
 }


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/SpringSecurityContextDeprecatedTest.java
similarity index 82%
rename from java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextTest.java
rename to java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextDeprecatedTest.java
index 28528eef..0f2bbaf5 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/SpringSecurityContextDeprecatedTest.java
@@ -20,14 +20,12 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 
-import static com.sap.cloud.security.config.Service.IAS;
-import static com.sap.cloud.security.token.TokenClaims.SUBJECT;
 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 {
+public class SpringSecurityContextDeprecatedTest {
 	AccessToken token;
 	SapIdToken sapIdToken;
 	private final ExecutorService executorService = Executors.newSingleThreadExecutor();
@@ -40,15 +38,6 @@ public class SpringSecurityContextTest {
 		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, NO_SCOPES);
@@ -76,19 +65,6 @@ public class SpringSecurityContextTest {
 		assertThat(SpringSecurityContext.getAccessToken().hasLocalScope("Scope3")).isFalse();
 	}
 
-	@Test
-	public void getAccessTokenReturnsNull_inCaseOfIasToken() {
-		setToken(sapIdToken, NO_SCOPES);
-		assertThat(SpringSecurityContext.getAccessToken()).isNull();
-	}
-
-	@Test
-	public void getTokenReturnsIasOidcToken() {
-		setToken(sapIdToken, NO_SCOPES);
-		assertThat(SpringSecurityContext.getToken().getService()).isEqualTo(IAS);
-		assertThat(SpringSecurityContext.getToken().getClaimAsString(SUBJECT)).isEqualTo("P176945");
-	}
-
 	@Test
 	public void clear_removesToken() {
 		setToken(token, NO_SCOPES);
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringXsuaaSecurityContextTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringXsuaaSecurityContextTest.java
new file mode 100644
index 00000000..317dc9bc
--- /dev/null
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringXsuaaSecurityContextTest.java
@@ -0,0 +1,126 @@
+package com.sap.cloud.security.adapter.spring;
+
+import com.sap.cloud.security.token.*;
+import com.sap.cloud.security.xsuaa.extractor.DefaultAuthoritiesExtractor;
+import com.sap.cloud.security.xsuaa.jwt.Base64JwtDecoder;
+import com.sap.cloud.security.xsuaa.jwt.DecodedJwt;
+import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoder;
+import org.apache.commons.io.IOUtils;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import static com.sap.cloud.security.config.Service.IAS;
+import static com.sap.cloud.security.token.TokenClaims.SUBJECT;
+import static com.sap.cloud.security.xsuaa.token.SpringSecurityContext.*;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+public class SpringXsuaaSecurityContextTest {
+	private final ExecutorService executorService = Executors.newSingleThreadExecutor();
+	AccessToken token;
+	SapIdToken sapIdToken;
+
+	@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 getTokenReturnsAnyToken() {
+		// initialize SpringSecurityContext with provided token
+		setToken(sapIdToken);
+		assertThat(SpringSecurityContext.getToken()).isEqualTo(sapIdToken);
+
+		setToken(token);
+		assertThat(SpringSecurityContext.getToken()).isEqualTo(token);
+	}
+
+	@Test
+	public void getAccessTokenReturnsXsuaaToken() {
+		setToken(token);
+		assertThat(SpringSecurityContext.getAccessToken()).isEqualTo(token);
+	}
+
+	@Test
+	public void getTokenReturnsIasOidcToken() {
+		setToken(sapIdToken);
+		assertThat(SpringSecurityContext.getToken().getService()).isEqualTo(IAS);
+		assertThat(SpringSecurityContext.getToken().getClaimAsString(SUBJECT)).isEqualTo("P176945");
+	}
+
+	@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) {
+		XsuaaJwtDecoder mockXsuaaJwtDecoder = Mockito.mock(XsuaaJwtDecoder.class);
+		when(mockXsuaaJwtDecoder.decode(token.getTokenValue()))
+				.thenReturn(convertTokenToOAuthJwt(token.getTokenValue()));
+
+		// initialize SpringSecurityContext with provided token
+		init(token.getTokenValue(), mockXsuaaJwtDecoder, new DefaultAuthoritiesExtractor());
+	}
+
+	@Nullable
+	public static Jwt convertTokenToOAuthJwt(String token) {
+		return parseJwt(decodeJwt(token));
+	}
+
+	private static Jwt parseJwt(DecodedJwt decodedJwt) {
+		JSONObject payload = new JSONObject(decodedJwt.getPayload());
+		JSONObject header = new JSONObject(decodedJwt.getHeader());
+		return new Jwt(decodedJwt.getEncodedToken(), Instant.ofEpochSecond(payload.optLong("iat")),
+				Instant.ofEpochSecond(payload.getLong("exp")),
+				header.toMap(), payload.toMap());
+	}
+
+	static DecodedJwt decodeJwt(String encodedJwtToken) {
+		return Base64JwtDecoder.getInstance().decode(encodedJwtToken);
+	}
+}
diff --git a/spring-security/src/test/java/com/sap/cloud/security/spring/token/SpringSecurityContextTest.java b/spring-security/src/test/java/com/sap/cloud/security/spring/token/SpringSecurityContextTest.java
index 7ec73877..93bb183b 100644
--- a/spring-security/src/test/java/com/sap/cloud/security/spring/token/SpringSecurityContextTest.java
+++ b/spring-security/src/test/java/com/sap/cloud/security/spring/token/SpringSecurityContextTest.java
@@ -1,7 +1,6 @@
 package com.sap.cloud.security.spring.token;
 
 import com.sap.cloud.security.test.JwtGenerator;
-import com.sap.cloud.security.spring.token.SpringSecurityContext;
 import com.sap.cloud.security.spring.token.authentication.HybridJwtDecoder;
 import com.sap.cloud.security.spring.token.authentication.XsuaaTokenAuthorizationConverter;
 import com.sap.cloud.security.token.Token;
@@ -80,6 +79,15 @@ class SpringSecurityContextTest {
 		assertEquals(xsuaaToken, SpringSecurityContext.getToken());
 	}
 
+	@Test
+	void getToken_fromJavaSpringSecurityContext() {
+		setToken(sapIdToken);
+		assertEquals(sapIdToken, com.sap.cloud.security.adapter.spring.SpringSecurityContext.getToken());
+
+		setToken(xsuaaToken);
+		assertEquals(xsuaaToken, com.sap.cloud.security.adapter.spring.SpringSecurityContext.getToken());
+	}
+
 	@Test
 	void getAccessToken() {
 		setToken(xsuaaToken);
@@ -110,9 +118,21 @@ class SpringSecurityContextTest {
 	void clear_removesToken() {
 		setToken(xsuaaToken);
 		SpringSecurityContext.clear();
+		com.sap.cloud.security.adapter.spring.SpringSecurityContext.clear();
 
 		assertThrows(AccessDeniedException.class, () -> SpringSecurityContext.getToken());
 		assertThrows(AccessDeniedException.class, () -> SpringSecurityContext.getAccessToken());
+
+		assertNull(com.sap.cloud.security.adapter.spring.SpringSecurityContext.getToken());
+		assertNull(com.sap.cloud.security.adapter.spring.SpringSecurityContext.getAccessToken());
+	}
+
+	@Test
+	void clear_removesTokenFromJavaSpringSecurityContext() {
+		com.sap.cloud.security.adapter.spring.SpringSecurityContext.clear();
+
+		assertNull(com.sap.cloud.security.adapter.spring.SpringSecurityContext.getToken());
+		assertNull(com.sap.cloud.security.adapter.spring.SpringSecurityContext.getAccessToken());
 	}
 
 	// @Test
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContextTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContextTest.java
index 000014c1..bd2d1b5c 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContextTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/SpringSecurityContextTest.java
@@ -8,8 +8,6 @@ import org.junit.Test;
 import org.mockito.Mockito;
 import org.springframework.security.access.AccessDeniedException;
 import org.springframework.security.oauth2.jwt.Jwt;
-import org.springframework.security.oauth2.jwt.JwtDecoder;
-import org.springframework.security.oauth2.jwt.JwtException;
 
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -41,13 +39,7 @@ public class SpringSecurityContextTest {
 
 	@Test(expected = IllegalArgumentException.class) // Passed JwtDecoder instance must be of type 'XsuaaJwtDecoder'
 	public void initSecurityContextRaiseExceptionIfNotXsuaaJwtDecoder() {
-		String message = "";
-		SpringSecurityContext.init(token_1.getTokenValue(), new JwtDecoder() {
-			@Override
-			public Jwt decode(String s) throws JwtException {
-				return token_1;
-			}
-		}, new DefaultAuthoritiesExtractor());
+		SpringSecurityContext.init(token_1.getTokenValue(), s -> token_1, new DefaultAuthoritiesExtractor());
 	}
 
 	/**