Repository: SAP/cloud-security-xsuaa-integration
Pull request #803 (2022-01-28 15:40:25)

Support uaa token validation - NGPBUG-175120


Patch:
diff --git a/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java b/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java
index f565c6e7..da0647e7 100644
--- a/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java
+++ b/java-security/src/main/java/com/sap/cloud/security/servlet/HybridTokenFactory.java
@@ -99,9 +99,10 @@ public class HybridTokenFactory implements TokenFactory {
 	 */
 	private static boolean isXsuaaToken(DecodedJwt decodedJwt) {
 		String jwtPayload = decodedJwt.getPayload().toLowerCase();
-		return jwtPayload.contains(EXTERNAL_ATTRIBUTE)
+		return (jwtPayload.contains(EXTERNAL_ATTRIBUTE)
 				&& jwtPayload.contains(EXTERNAL_ATTRIBUTE_ENHANCER)
-				&& jwtPayload.contains("xsuaa");
+				&& jwtPayload.contains("xsuaa"))
+				|| jwtPayload.contains("\"zid\":\"uaa\",");
 	}
 
 	private static String removeBearer(@Nonnull String jwtToken) {
diff --git a/pom.xml b/pom.xml
index d149cb3a..42a9062d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -65,7 +65,7 @@
 		<spring.security.jwt.version>1.1.1.RELEASE</spring.security.jwt.version>
 		<reactor.version>3.4.14</reactor.version>
 		<log4j2.version>2.17.1</log4j2.version>
-		<slf4j.api.version>1.7.33</slf4j.api.version> <!--see also here http://www.slf4j.org/faq.html#changesInVersion18 -->
+		<slf4j.api.version>1.7.35</slf4j.api.version> <!--see also here http://www.slf4j.org/faq.html#changesInVersion18 -->
 		<org.json.version>20211205</org.json.version>
 		<google.jsr305.version>3.0.2</google.jsr305.version>
 		<apache.httpclient.version>4.5.13</apache.httpclient.version>


Test patch:
diff --git a/java-security-it/src/test/java/com/sap/cloud/security/test/integration/XsuaaIntegrationTest.java b/java-security-it/src/test/java/com/sap/cloud/security/test/integration/XsuaaIntegrationTest.java
index b7e03dcc..c4cc81ed 100644
--- a/java-security-it/src/test/java/com/sap/cloud/security/test/integration/XsuaaIntegrationTest.java
+++ b/java-security-it/src/test/java/com/sap/cloud/security/test/integration/XsuaaIntegrationTest.java
@@ -40,8 +40,8 @@ public class XsuaaIntegrationTest {
 			.setKeys("/publicKey.txt", "/privateKey.txt");
 
 	@Test
-	public void xsuaaTokenValidationSucceeds_withXsuaaCombiningValidator() throws IOException {
-		OAuth2ServiceConfigurationBuilder configuration = rule.getConfigurationBuilderFromFile(
+	public void xsuaaTokenValidationSucceeds_withXsuaaCombiningValidator() {
+		OAuth2ServiceConfigurationBuilder configuration = rule.getOAuth2ServiceConfigurationBuilderFromFile(
 				"/xsuaa/vcap_services-single.json");
 		Token token = rule.getJwtGeneratorFromFile("/xsuaa/token.json").createToken();
 
@@ -52,7 +52,7 @@ public class XsuaaIntegrationTest {
 
 	@Test
 	public void xsaTokenValidationSucceeds_withXsuaaCombiningValidator() throws IOException {
-		OAuth2ServiceConfiguration configuration = rule.getConfigurationBuilderFromFile(
+		OAuth2ServiceConfiguration configuration = rule.getOAuth2ServiceConfigurationBuilderFromFile(
 				"/xsa-simple/vcap_services-single.json")
 				.runInLegacyMode(true)
 				.build();
@@ -66,7 +66,7 @@ public class XsuaaIntegrationTest {
 	}
 
 	@Test
-	public void xsuaaTokenValidationFails_withIasCombiningValidator() throws IOException {
+	public void xsuaaTokenValidationFails_withIasCombiningValidator() {
 		OAuth2ServiceConfiguration configuration = rule.getOAuth2ServiceConfigurationBuilderFromFile(
 				"/ias-simple/vcap_services-single.json")
 				.withDomains("myauth.com")
@@ -86,6 +86,20 @@ public class XsuaaIntegrationTest {
 				"Issuer is not trusted because issuer 'http://auth.com' doesn't match any of these domains '[myauth.com]' of the identity provider");
 	}
 
+	@Test
+	public void uaaTokenValidationSucceeds_withXsuaaCombiningValidator() {
+		OAuth2ServiceConfigurationBuilder configuration = rule.getOAuth2ServiceConfigurationBuilderFromFile(
+				"/uaa/vcap_services.json");
+		Token token = rule.getJwtGeneratorFromFile("/uaa/token.json")
+				//.withHeaderParameter("jku", "http://auth.com/token_keys")  // required to create uaaAccessTokenRSA.txt
+				//.withHeaderParameter("kid", "key-id-0")
+				.createToken();
+
+		CombiningValidator<Token> tokenValidator = JwtValidatorBuilder.getInstance(configuration.build()).build();
+		ValidationResult result = tokenValidator.validate(token);
+		assertThat(result.isValid()).isTrue();
+	}
+
 	@Test
 	public void createToken_withCorrectVerificationKey_tokenIsValid() throws IOException {
 		String publicKey = IOUtils.resourceToString("/publicKey.txt", StandardCharsets.UTF_8);
diff --git a/java-security-it/src/test/resources/uaa/token.json b/java-security-it/src/test/resources/uaa/token.json
new file mode 100644
index 00000000..16cd1079
--- /dev/null
+++ b/java-security-it/src/test/resources/uaa/token.json
@@ -0,0 +1,20 @@
+{
+  "header": {
+    "alg": "RS256",
+    "jku": "http://auth.com/token_keys",
+    "kid": "key-id-0"
+  },
+  "payload": {
+    "aud": [
+      "dashboard_client-Id"
+    ],
+    "azp": "dashboard_client-Id",
+    "exp": "6974031600",
+    "grant_type": "authorization_code",
+    "iss": "http://auth.com/oauth/token",
+    "origin": "sap.ids",
+    "user_name": "testUser",
+    "user_id": "testUserId",
+    "zid": "uaa"
+  }
+}
\ No newline at end of file
diff --git a/java-security-it/src/test/resources/uaa/vcap_services.json b/java-security-it/src/test/resources/uaa/vcap_services.json
new file mode 100644
index 00000000..3f68cc4c
--- /dev/null
+++ b/java-security-it/src/test/resources/uaa/vcap_services.json
@@ -0,0 +1,28 @@
+{
+  "xsuaa": [
+   {
+    "binding_name": null,
+    "credentials": {
+     "clientid": "dashboard_client-Id",
+     "clientsecret": "",
+     "identityzone": "uaa",
+     "identityzoneid": "uaa",
+     "tenantmode": "dedicated",
+     "uaadomain": "auth.com",
+     "url": "http://auth.com",
+     "verificationkey": "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAm1QaZzMjtEfHdimrHP3/2Yr+1z685eiOUlwybRVG9i8wsgOUh+PUGuQL8hgulLZWXU5MbwBLTECAEMQbcRTNVTolkq4i67EP6JesHJIFADbK1Ni0KuMcPuiyOLvDKiDEMnYG1XP3X3WCNfsCVT9YoU+lWIrZr/ZsIvQri8jczr4RkynbTBsPaAOygPUlipqDrpadMO1momNCbea/o6GPn38LxEw609ItfgDGhL6f/yVid5pFzZQWb+9l6mCuJww0hnhO6gt6Rv98OWDty9G0frWAPyEfuIW9B+mR/2vGhyU9IbbWpvFXiy9RVbbsM538TCjd5JF2dJvxy24addC4oQIDAQAB-----END PUBLIC KEY-----",
+     "xsappname": "dashboard_client"
+    },
+    "instance_name": "user-provided",
+    "label": "xsuaa",
+    "name": "example-xsuaa",
+    "plan": "broker",
+    "provider": null,
+    "syslog_drain_url": null,
+    "tags": [
+     "xsuaa"
+    ],
+    "volume_mounts": []
+   }
+  ]
+ }
\ No newline at end of file
diff --git a/java-security/src/test/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticatorTest.java b/java-security/src/test/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticatorTest.java
index 0e5248cf..65c0121d 100644
--- a/java-security/src/test/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticatorTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticatorTest.java
@@ -5,8 +5,6 @@
  */
 package com.sap.cloud.security.servlet;
 
-import com.sap.cloud.security.config.ClientIdentity;
-import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
 import com.sap.cloud.security.config.OAuth2ServiceConfigurationBuilder;
 import com.sap.cloud.security.config.Service;
 import com.sap.cloud.security.token.SapIdToken;
@@ -44,9 +42,10 @@ public class XsuaaTokenAuthenticatorTest {
 
 	private final XsuaaToken xsuaaToken;
 	private final XsuaaToken invalidToken;
+	private final XsuaaToken uaaToken;
 	private final SapIdToken iasToken;
 	private CloseableHttpClient mockHttpClient;
-	private OAuth2ServiceConfiguration oAuth2ServiceConfiguration;
+	private OAuth2ServiceConfigurationBuilder oAuth2ServiceConfigBuilder;
 
 	private AbstractTokenAuthenticator cut;
 
@@ -55,6 +54,7 @@ public class XsuaaTokenAuthenticatorTest {
 		invalidToken = new XsuaaToken(
 				IOUtils.resourceToString("/xsuaaCCAccessTokenRSA256.txt", UTF_8));
 		iasToken = new SapIdToken(IOUtils.resourceToString("/iasOidcTokenRSA256.txt", UTF_8));
+		uaaToken = new XsuaaToken(IOUtils.resourceToString("/uaaAccessTokenRSA256.txt", UTF_8));
 	}
 
 	@Before
@@ -71,18 +71,17 @@ public class XsuaaTokenAuthenticatorTest {
 								+ "\", \"expires_in\" : 43199}");
 		when(mockHttpClient.execute(any(HttpPost.class))).thenReturn(xsuaaTokenResponse);
 
-		oAuth2ServiceConfiguration = OAuth2ServiceConfigurationBuilder
+		oAuth2ServiceConfigBuilder = OAuth2ServiceConfigurationBuilder
 				.forService(Service.XSUAA)
 				.withDomains("auth.com")
 				.withProperty(XSUAA.APP_ID, "appId")
 				.withClientId("clientId")
 				.withClientSecret("mySecret")
-				.withUrl("https://myauth.com")
-				.build();
+				.withUrl("https://myauth.com");
 
 		cut = new XsuaaTokenAuthenticator()
 				.withHttpClient(mockHttpClient)
-				.withServiceConfiguration(oAuth2ServiceConfiguration);
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 	}
 
 	@Test
@@ -105,13 +104,29 @@ public class XsuaaTokenAuthenticatorTest {
 				.execute(() -> {
 					cut = new XsuaaTokenAuthenticator()
 							.withHttpClient(null)
-							.withServiceConfiguration(oAuth2ServiceConfiguration);
+							.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 					response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE);
 				});
 		assertEquals("IAS token validation is not supported: setup is malicious.",
 				response[0].getUnauthenticatedReason());
 	}
 
+	@Test
+	public void validateUaaToken() {
+		HttpServletRequest httpRequest = createRequestWithToken(uaaToken.getTokenValue());
+
+		cut = new XsuaaTokenAuthenticator()
+				.withHttpClient(mockHttpClient)
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.withClientId("dashboard_client-Id").build());
+
+		TokenAuthenticationResult response = cut.validateRequest(httpRequest, HTTP_RESPONSE);
+
+		assertThat(response.getUnauthenticatedReason()).isEmpty();
+		assertThat(response.isAuthenticated()).isTrue();
+		assertThat(response.getToken()).isSameAs(SecurityContext.getToken());
+		assertThat(response.getToken().getService()).isEqualTo(Service.XSUAA);
+	}
+
 	@Test
 	public void validateIasToken() throws Exception {
 		HttpServletRequest httpRequest = createRequestWithToken(iasToken.getTokenValue());
@@ -121,10 +136,10 @@ public class XsuaaTokenAuthenticatorTest {
 				.execute(() -> {
 					cut = new XsuaaTokenAuthenticator()
 							.withHttpClient(mockHttpClient)
-							.withServiceConfiguration(oAuth2ServiceConfiguration);
+							.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 					response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE);
 				});
-		assertThat(((XsuaaToken) response[0].getToken()).isExpired()).isFalse();
+		assertThat(response[0].getToken().isExpired()).isFalse();
 	}
 
 	@Test
@@ -200,7 +215,7 @@ public class XsuaaTokenAuthenticatorTest {
 
 		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
 				.withHttpClient(mockHttpClient)
-				.withServiceConfiguration(oAuth2ServiceConfiguration);
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 
 		HttpServletRequest httpRequest = createRequestWithToken(iasToken.getTokenValue());
 		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
@@ -216,7 +231,7 @@ public class XsuaaTokenAuthenticatorTest {
 
 		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
 				.withHttpClient(mockHttpClient)
-				.withServiceConfiguration(oAuth2ServiceConfiguration);
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 
 		HttpServletRequest httpRequest = createRequestWithToken(iasToken.getTokenValue());
 		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
@@ -232,7 +247,7 @@ public class XsuaaTokenAuthenticatorTest {
 
 		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
 				.withHttpClient(mockHttpClient)
-				.withServiceConfiguration(oAuth2ServiceConfiguration);
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 
 		HttpServletRequest httpRequest = createRequestWithToken(xsuaaToken.getTokenValue());
 		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
@@ -248,7 +263,7 @@ public class XsuaaTokenAuthenticatorTest {
 
 		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
 				.withHttpClient(mockHttpClient)
-				.withServiceConfiguration(oAuth2ServiceConfiguration);
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 
 		HttpServletRequest httpRequest = createRequestWithToken(invalidToken.getTokenValue());
 		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
@@ -265,7 +280,7 @@ public class XsuaaTokenAuthenticatorTest {
 
 		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
 				.withHttpClient(mockHttpClient)
-				.withServiceConfiguration(oAuth2ServiceConfiguration);
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 
 		HttpServletRequest httpRequest = createRequestWithToken(xsuaaToken.getTokenValue());
 		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
@@ -281,7 +296,7 @@ public class XsuaaTokenAuthenticatorTest {
 
 		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
 				.withHttpClient(mockHttpClient)
-				.withServiceConfiguration(oAuth2ServiceConfiguration);
+				.withServiceConfiguration(oAuth2ServiceConfigBuilder.build());
 
 		HttpServletRequest httpRequest = createRequestWithToken(invalidToken.getTokenValue());
 		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
diff --git a/java-security/src/test/resources/uaaAccessTokenRSA256.txt b/java-security/src/test/resources/uaaAccessTokenRSA256.txt
new file mode 100644
index 00000000..137f2afe
--- /dev/null
+++ b/java-security/src/test/resources/uaaAccessTokenRSA256.txt
@@ -0,0 +1,1 @@
+eyJqa3UiOiJodHRwOi8vYXV0aC5jb20vdG9rZW5fa2V5cyIsImtpZCI6ImtleS1pZC0wIiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOlsiZGFzaGJvYXJkX2NsaWVudC1JZCJdLCJ6aWQiOiJ1YWEiLCJncmFudF90eXBlIjoiYXV0aG9yaXphdGlvbl9jb2RlIiwidXNlcl9pZCI6InRlc3RVc2VySWQiLCJhenAiOiJkYXNoYm9hcmRfY2xpZW50LUlkIiwidXNlcl9uYW1lIjoidGVzdFVzZXIiLCJvcmlnaW4iOiJzYXAuaWRzIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MjcwMCIsImV4cCI6Njk3NDAzMTYwMH0.dA9gjVpX3G099gtRwIQDvppLyIaO5WsxAfmq71CTNKJv2J6LXgQOyhOk0NKgmQHxCvL-UEqnAImRXK1EF9ewgI_MD6Bn0N9mSGV0HmroM3es9go8t4f5DWNPllgTTgaiO34dIGEuFXkhn3uFVy34ErRhjAxuhknGg571jW2hg0jncDwMuC1mK_h7gvH-bb1ENmxG7NXSNZAQiJ0IlIZxixGmo1cJZP9V1oNugnE4mLJmB0I_7fRm9iYm0kBfLbOV_NgJ32AQ7I44SDoRlO1k5HvxKOPWwn2ZlSEoFiPnfsO9N4wrK1TZ-5kH8y6NpSI6R4hnJbmomZruEXnQ0MitpQ
\ No newline at end of file