Repository: SAP/cloud-security-xsuaa-integration
Pull request #485 (2021-03-09 11:58:16)

SpringSecurityContext should also support Spring OIDC id tokens


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3835c92..0d381e8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,11 +3,15 @@
 All notable changes to this project will be documented in this file.
 
 ## 2.8.8
+- [java-security-test] and java samples
+  - jetty 9.4.36.v20210114 --> 9.4.38.v20210224 (seems to be incompatible with javax.servlet-api 3.1.0)
+  - javax.servlet:javax.servlet-api 3.1.0 --> 4.0.1 (recommended version)
 - [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
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 505ecaaf..cad60757 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
@@ -51,23 +51,22 @@ public class SpringSecurityContext {
 		if (Objects.nonNull(authentication) && authentication.isAuthenticated()) {
 			try {
 				if (authentication.getDetails() != null && authentication.getDetails().getClass()
-						.getName().equals("org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails")) {
+						.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());
+				String principalClass = authentication.getPrincipal().getClass().getName();
+				LOGGER.debug("Try to fetch token from SecurityContextHolder.getPrincipal() of type {}.",
+						authentication.getPrincipal().getClass().getName());
+				if (principalClass.startsWith("com.sap.cloud.security.xsuaa.token.")) {
 					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());
+				} else if (principalClass.startsWith("com.sap.cloud.security.token.")) {
 					return (Token) authentication.getPrincipal();
+				} else if (principalClass.startsWith("org.springframework.security.oauth2.core.oidc.user")) {
+					return getSpringOidcIdToken(authentication);
 				}
 			} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
 				LOGGER.error("Does not yet support (Tokens of class: {})", authentication.getPrincipal().getClass());
@@ -105,6 +104,15 @@ public class SpringSecurityContext {
 		return Token.create(encodedToken);
 	}
 
+	static Token getSpringOidcIdToken(Authentication authentication)
+			throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
+		Method getIdToken = authentication.getPrincipal().getClass().getMethod("getIdToken");
+		Object oidcToken = getIdToken.invoke(authentication.getPrincipal());
+		Method getTokenValue = oidcToken.getClass().getMethod("getTokenValue");
+		String encodedToken = (String) getTokenValue.invoke(oidcToken);
+		return Token.create(encodedToken);
+	}
+
 	/**
 	 * Returns the token using {@link SecurityContextHolder}.
 	 *
diff --git a/pom.xml b/pom.xml
index 09b0bd74..a0349672 100644
--- a/pom.xml
+++ b/pom.xml
@@ -69,7 +69,7 @@
 		<apache.httpclient.version>4.5.13</apache.httpclient.version>
 		<caffeine.version>2.8.8</caffeine.version>
 		<commons.io.version>2.8.0</commons.io.version>
-		<javax.servlet.api.version>3.1.0</javax.servlet.api.version>
+		<javax.servlet.api.version>4.0.1</javax.servlet.api.version>
 		<mockwebserver.version>3.9.1</mockwebserver.version>
 		<junit.version>4.13.1</junit.version>
 		<junit-jupiter.version>5.7.0</junit-jupiter.version>
diff --git a/samples/java-security-usage-ias/pom.xml b/samples/java-security-usage-ias/pom.xml
index 02854eea..9de384ba 100755
--- a/samples/java-security-usage-ias/pom.xml
+++ b/samples/java-security-usage-ias/pom.xml
@@ -27,7 +27,7 @@
         <sap.cloud.security.version>2.8.8</sap.cloud.security.version>
         <slf4j.api.version>1.7.30</slf4j.api.version>
         <apache.httpclient.version>4.5.13</apache.httpclient.version>
-        <javax.servlet.api.version>3.1.0</javax.servlet.api.version>
+        <javax.servlet.api.version>4.0.1</javax.servlet.api.version>
         <assertj.version>3.13.2</assertj.version>
         <junit.version>5.7.0</junit.version>
     </properties>
diff --git a/samples/java-security-usage/pom.xml b/samples/java-security-usage/pom.xml
index eb74f19e..8864019d 100755
--- a/samples/java-security-usage/pom.xml
+++ b/samples/java-security-usage/pom.xml
@@ -27,7 +27,7 @@
         <sap.cloud.security.version>2.8.8</sap.cloud.security.version>
         <slf4j.api.version>1.7.30</slf4j.api.version>
         <apache.httpclient.version>4.5.13</apache.httpclient.version>
-        <javax.servlet.api.version>3.1.0</javax.servlet.api.version>
+        <javax.servlet.api.version>4.0.1</javax.servlet.api.version>
         <assertj.version>3.13.2</assertj.version>
         <junit.version>5.7.0</junit.version>
     </properties>
@@ -79,5 +79,12 @@
     </dependencies>
     <build>
         <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>3.0.0-M4</version>
+            </plugin>
+        </plugins>
     </build>
 </project>
diff --git a/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServlet.java b/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServlet.java
index d80589f3..5cc4a62b 100644
--- a/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServlet.java
+++ b/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServlet.java
@@ -15,7 +15,7 @@ import java.io.IOException;
 @WebServlet(HelloJavaServlet.ENDPOINT)
 public class HelloJavaServlet extends HttpServlet {
 	static final String ENDPOINT = "/hello-java-security";
-	private static Logger LOGGER = LoggerFactory.getLogger(HelloJavaServlet.class);
+	private static final Logger LOGGER = LoggerFactory.getLogger(HelloJavaServlet.class);
 
 	/**
 	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
@@ -31,9 +31,9 @@ public class HelloJavaServlet extends HttpServlet {
 			message.append(token.getClaimAsString(TokenClaims.EMAIL));
 			message.append("') can access the application with the following scopes: '");
 			message.append(token.getClaimAsStringList(TokenClaims.XSUAA.SCOPES));
-			message.append("'.");
+			message.append("'. ");
 			// for authorization check you need the AccessToken interface (instead of Token)
-			message.append("having scope '$XSAPPNAME.Read'? " + token.hasLocalScope("Read"));
+			message.append("Having scope '$XSAPPNAME.Read'? " + token.hasLocalScope("Read"));
 			response.getWriter().write(message.toString());
 			response.setStatus(HttpServletResponse.SC_OK);
 		} catch (final IOException e) {
diff --git a/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServletScopeProtected.java b/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServletScopeProtected.java
index 061dbc2e..20d76c94 100644
--- a/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServletScopeProtected.java
+++ b/samples/java-security-usage/src/main/java/com/sap/cloud/security/samples/HelloJavaServletScopeProtected.java
@@ -1,8 +1,6 @@
 package com.sap.cloud.security.samples;
 
 import com.sap.cloud.security.token.SecurityContext;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServlet;
@@ -13,7 +11,6 @@ import java.io.IOException;
 @WebServlet(HelloJavaServletScopeProtected.ENDPOINT)
 public class HelloJavaServletScopeProtected extends HttpServlet {
 	static final String ENDPOINT = "/hello-java-security-authz";
-	private static Logger LOGGER = LoggerFactory.getLogger(HelloJavaServletScopeProtected.class);
 
 	/**
 	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
diff --git a/samples/java-tokenclient-usage/pom.xml b/samples/java-tokenclient-usage/pom.xml
index e90e3bab..bb0adf9d 100755
--- a/samples/java-tokenclient-usage/pom.xml
+++ b/samples/java-tokenclient-usage/pom.xml
@@ -11,7 +11,7 @@
         <maven.compiler.target>1.8</maven.compiler.target>
         <sap.cloud.security.version>2.8.8</sap.cloud.security.version>
         <apache.httpclient.version>4.5.13</apache.httpclient.version>
-        <javax.servlet.api.version>3.1.0</javax.servlet.api.version>
+        <javax.servlet.api.version>4.0.1</javax.servlet.api.version>
     </properties>
 
     <dependencies>
diff --git a/samples/sap-java-buildpack-api-usage/pom.xml b/samples/sap-java-buildpack-api-usage/pom.xml
index 0cb1cfef..eca853e7 100755
--- a/samples/sap-java-buildpack-api-usage/pom.xml
+++ b/samples/sap-java-buildpack-api-usage/pom.xml
@@ -9,7 +9,7 @@
 	<properties>
 		<maven.compiler.source>1.8</maven.compiler.source>
 		<maven.compiler.target>1.8</maven.compiler.target>
-		<javax.servlet.api.version>3.1.0</javax.servlet.api.version>
+		<javax.servlet.api.version>4.0.1</javax.servlet.api.version>
 	</properties>
 
 	<dependencies>


Test patch:
diff --git a/java-security-test/pom.xml b/java-security-test/pom.xml
index b073a456..4af8f849 100644
--- a/java-security-test/pom.xml
+++ b/java-security-test/pom.xml
@@ -16,7 +16,7 @@
 	<packaging>jar</packaging>
 
 	<properties>
-		<jetty.version>9.4.36.v20210114</jetty.version>
+		<jetty.version>9.4.38.v20210224</jetty.version>
 		<wiremock.version>2.27.2</wiremock.version>
 	</properties>
 
diff --git a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
index ed45f8c3..6cf1e1c6 100644
--- a/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
+++ b/java-security-test/src/test/java/com/sap/cloud/security/test/SecurityTestRuleTest.java
@@ -97,7 +97,6 @@ public class SecurityTestRuleTest {
 	@Test
 	public void testRuleIsInitializedCorrectly() {
 		assertThat(cut.getApplicationServerUri()).isEqualTo("http://localhost:" + APPLICATION_SERVER_PORT);
-		// assertThat(cut.getWireMockRule()).isNotNull();
 		assertThat(cut.getWireMockServer()).isNotNull();
 		assertThat(cut.createToken().getTokenValue())
 				.isEqualTo(cut.getPreconfiguredJwtGenerator().createToken().getTokenValue());
@@ -138,14 +137,6 @@ public class SecurityTestRuleTest {
 		assertThat(token.getClaimAsString(TokenClaims.ISSUER)).isEqualTo(baseUrl);
 	}
 
-	@Test
-	public void servletFilterServesTestServlet() throws IOException {
-		HttpGet httpGet = new HttpGet(cut.getApplicationServerUri() + "/hi");
-		try (CloseableHttpResponse response = HttpClients.createDefault().execute(httpGet)) {
-			assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_UNAUTHORIZED);
-		}
-	}
-
 	@Test
 	public void setKeys_invalidPath_throwsException() {
 		assertThatThrownBy(() -> SecurityTestRule.getInstance(XSUAA)
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringOAuth2ClientSecurityContextTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringOAuth2ClientSecurityContextTest.java
new file mode 100644
index 00000000..4d100f04
--- /dev/null
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringOAuth2ClientSecurityContextTest.java
@@ -0,0 +1,62 @@
+package com.sap.cloud.security.adapter.spring;
+
+import com.sap.cloud.security.token.SapIdToken;
+import com.sap.cloud.security.token.Token;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+
+import java.io.IOException;
+import java.time.Instant;
+
+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 java.util.Collections.singleton;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class SpringOAuth2ClientSecurityContextTest {
+	SapIdToken sapIdToken;
+
+	@BeforeEach
+	public void setUp() throws IOException {
+		sapIdToken = new SapIdToken(IOUtils.resourceToString("/iasOidcTokenRSA256.txt", UTF_8));
+		SpringSecurityContext.clear();
+	}
+
+	@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(sapIdToken);
+		SpringSecurityContext.clear();
+
+		assertThat(SpringSecurityContext.getToken()).isNull();
+	}
+
+	private static void setToken(Token token) {
+		OidcIdToken oidcIdToken = new OidcIdToken(token.getTokenValue(), Instant.now(), Instant.now().plusSeconds(1L),
+				token.getClaims());
+		SecurityContextHolder.getContext().setAuthentication(getMockAuthentication(oidcIdToken));
+	}
+
+	static Authentication getMockAuthentication(OidcIdToken oidcIdToken) {
+		Authentication authentication = mock(Authentication.class);
+		when(authentication.getPrincipal())
+				.thenReturn(new DefaultOidcUser(singleton(new SimpleGrantedAuthority("openid")), oidcIdToken));
+		when(authentication.isAuthenticated()).thenReturn(true);
+		return authentication;
+	}
+}
diff --git a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextDeprecatedTest.java b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextDeprecatedTest.java
index 0f2bbaf5..c4ac1c17 100644
--- a/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextDeprecatedTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/adapter/spring/SpringSecurityContextDeprecatedTest.java
@@ -2,8 +2,8 @@ package com.sap.cloud.security.adapter.spring;
 
 import com.sap.cloud.security.token.*;
 import org.apache.commons.io.IOUtils;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.context.SecurityContextImpl;
@@ -31,7 +31,7 @@ public class SpringSecurityContextDeprecatedTest {
 	private final ExecutorService executorService = Executors.newSingleThreadExecutor();
 	private static final Set<String> NO_SCOPES = Collections.EMPTY_SET;
 
-	@Before
+	@BeforeEach
 	public void setUp() throws IOException {
 		token = new XsuaaToken(IOUtils.resourceToString("/xsuaaUserAccessTokenRSA256.txt", UTF_8));
 		sapIdToken = new SapIdToken(IOUtils.resourceToString("/iasOidcTokenRSA256.txt", UTF_8));
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
index 317dc9bc..d0577ef5 100644
--- 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
@@ -7,8 +7,8 @@ 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.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 import org.springframework.lang.Nullable;
 import org.springframework.security.oauth2.jwt.Jwt;
@@ -32,7 +32,7 @@ public class SpringXsuaaSecurityContextTest {
 	AccessToken token;
 	SapIdToken sapIdToken;
 
-	@Before
+	@BeforeEach
 	public void setUp() throws IOException {
 		token = new XsuaaToken(IOUtils.resourceToString("/xsuaaUserAccessTokenRSA256.txt", UTF_8));
 		sapIdToken = new SapIdToken(IOUtils.resourceToString("/iasOidcTokenRSA256.txt", UTF_8));
diff --git a/samples/java-security-usage/src/test/java/com/sap/cloud/security/samples/HelloJavaServletIntegrationTest.java b/samples/java-security-usage/src/test/java/com/sap/cloud/security/samples/HelloJavaServletIntegrationTest.java
index c476b1b2..74b5fd05 100644
--- a/samples/java-security-usage/src/test/java/com/sap/cloud/security/samples/HelloJavaServletIntegrationTest.java
+++ b/samples/java-security-usage/src/test/java/com/sap/cloud/security/samples/HelloJavaServletIntegrationTest.java
@@ -25,7 +25,9 @@ public class HelloJavaServletIntegrationTest {
 
 	@RegisterExtension
 	static SecurityTestExtension extension = SecurityTestExtension.forService(XSUAA)
-			.useApplicationServer().addApplicationServlet(HelloJavaServlet.class, HelloJavaServlet.ENDPOINT);
+			.useApplicationServer()
+			.addApplicationServlet(HelloJavaServlet.class, HelloJavaServlet.ENDPOINT)
+			.addApplicationServlet(HelloJavaServletScopeProtected.class, HelloJavaServletScopeProtected.ENDPOINT);
 
 	@AfterEach
 	public void tearDown() {
@@ -54,9 +56,9 @@ public class HelloJavaServletIntegrationTest {
 				.withClaimValue(GRANT_TYPE, GRANT_TYPE_CLIENT_CREDENTIALS)
 				.createToken()
 				.getTokenValue();
-		HttpGet request = createGetRequest(jwt);
+		HttpGet request = createGetRequest(jwt, HelloJavaServletScopeProtected.ENDPOINT);
 		try (CloseableHttpResponse response = HttpClients.createDefault().execute(request)) {
-			assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN); // 403
+ 			assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_FORBIDDEN); // 403
 		}
 	}
 
@@ -71,13 +73,20 @@ public class HelloJavaServletIntegrationTest {
 		HttpGet request = createGetRequest(jwt);
 		try (CloseableHttpResponse response = HttpClients.createDefault().execute(request)) {
 			assertThat(response.getStatusLine().getStatusCode()).isEqualTo(HttpStatus.SC_OK); // 200
-			assertThat(EntityUtils.toString(response.getEntity(), "UTF-8"))
-					.isEqualTo("You ('tester@mail.com') can access the application with the following scopes: '[xsapp!t0815.Read]'.");
+			String responseBody = EntityUtils.toString(response.getEntity());
+			assertThat(responseBody)
+					.contains("You ('tester@mail.com') can access the application with the following scopes: '[xsapp!t0815.Read]'.");
+			assertThat(responseBody)
+					.contains("Having scope '$XSAPPNAME.Read'? true");
 		}
 	}
 
 	private HttpGet createGetRequest(String accessToken) {
-		HttpGet httpGet = new HttpGet(extension.getContext().getApplicationServerUri() + HelloJavaServlet.ENDPOINT);
+		return createGetRequest(accessToken, HelloJavaServlet.ENDPOINT);
+	}
+
+	private HttpGet createGetRequest(String accessToken, String endpoint) {
+		HttpGet httpGet = new HttpGet(extension.getContext().getApplicationServerUri() + endpoint);
 		if(accessToken != null) {
 			httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
 		}