Repository: SAP/cloud-security-xsuaa-integration
Pull request #787 (2022-01-05 12:52:29)

Support oidc tokens from single tenant apps w/o zone_uuid claim
In case of single-tenant applications ``iss`` claim in oidc token claim matches the identity provider url (``OAuth2ServiceConfiguration.getUrl())``. In that case ``x-zone_uuid`` header is not required to request the ``jwks_uri`` endpoint to retrieve token keys.


Patch:
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java
index a5c39e6c..321feb70 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidator.java
@@ -63,7 +63,7 @@ class JwtSignatureValidator implements Validator<Token> {
 		String jwksUri;
 		String keyId;
 
-		if (Service.IAS == configuration.getService() && token.getZoneId() == null) { // lgtm[java/dereferenced-value-may-be-null]
+		if (Service.IAS == configuration.getService() && !token.getIssuer().equals("" + configuration.getUrl()) && token.getZoneId() == null) { // lgtm[java/dereferenced-value-may-be-null]
 			return createInvalid("Error occurred during signature validation: OIDC token must provide zone_uuid.");
 		}
 		try {
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java
index 2b1ff86f..a5723b79 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/DefaultOAuth2TokenKeyService.java
@@ -15,6 +15,7 @@ import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.impl.client.CloseableHttpClient;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import java.io.IOException;
 import java.net.URI;
 
@@ -34,10 +35,12 @@ public class DefaultOAuth2TokenKeyService implements OAuth2TokenKeyService {
 	}
 
 	@Override
-	public String retrieveTokenKeys(URI tokenKeysEndpointUri, String zoneId) throws OAuth2ServiceException {
+	public String retrieveTokenKeys(URI tokenKeysEndpointUri, @Nullable String zoneId) throws OAuth2ServiceException {
 		Assertions.assertNotNull(tokenKeysEndpointUri, "Token key endpoint must not be null!");
 		HttpUriRequest request = new HttpGet(tokenKeysEndpointUri);
-		request.addHeader(X_ZONE_UUID, zoneId != null ? zoneId : "");
+		if (zoneId != null) {
+			request.addHeader(X_ZONE_UUID, zoneId);
+		}
 		try (CloseableHttpResponse response = httpClient.execute(request)) {
 			String bodyAsString = HttpClientUtil.extractResponseBodyAsString(response);
 			int statusCode = response.getStatusLine().getStatusCode();
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java
index 34a18b5d..c6a1ee18 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/OAuth2TokenKeyService.java
@@ -9,6 +9,9 @@ import java.net.URI;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+/**
+ * Service that targets Identity service (xsuaa and identity) to request Json Web Keys.
+ */
 public interface OAuth2TokenKeyService {
 
 	/**
diff --git a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
index 39110544..62a384e7 100644
--- a/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
+++ b/token-client/src/main/java/com/sap/cloud/security/xsuaa/client/SpringOAuth2TokenKeyService.java
@@ -11,6 +11,7 @@ import org.springframework.web.client.HttpStatusCodeException;
 import org.springframework.web.client.RestOperations;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import java.net.URI;
 import java.util.Collections;
 
@@ -26,13 +27,15 @@ public class SpringOAuth2TokenKeyService implements OAuth2TokenKeyService {
 	}
 
 	@Override
-	public String retrieveTokenKeys(URI tokenKeysEndpointUri, String zoneId) throws OAuth2ServiceException {
+	public String retrieveTokenKeys(URI tokenKeysEndpointUri, @Nullable String zoneId) throws OAuth2ServiceException {
 		Assertions.assertNotNull(tokenKeysEndpointUri, "Token key endpoint must not be null!");
 		try {
 			// create headers
 			HttpHeaders headers = new HttpHeaders();
 			headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
-			headers.set(X_ZONE_UUID, zoneId != null ? zoneId : "");
+			if (zoneId != null) {
+				headers.set(X_ZONE_UUID, zoneId);
+			}
 
 			ResponseEntity<String> response = restOperations.exchange(
 					tokenKeysEndpointUri, GET, new HttpEntity(headers), String.class);


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidatorTest.java b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidatorTest.java
index 1d8757d6..161994e9 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidatorTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/validation/validators/JwtSignatureValidatorTest.java
@@ -32,10 +32,10 @@ public class JwtSignatureValidatorTest {
 	private Token iasToken;
 	private Token iasPaasToken;
 	private static final URI DUMMY_JKU_URI = URI.create("https://application.myauth.com/jwks_uri");
-	private static final String DUMMY_ZONE_ID = "the-zone_id";
 
 	private JwtSignatureValidator cut;
 	private OAuth2TokenKeyService tokenKeyServiceMock;
+	private OAuth2ServiceConfiguration mockConfiguration;
 
 	@Before
 	public void setup() throws IOException {
@@ -44,7 +44,7 @@ public class JwtSignatureValidatorTest {
 				"eyJraWQiOiJkZWZhdWx0LWtpZCIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiJQMTc2OTQ1IiwiYXVkIjoiVDAwMDMxMCIsInVzZXJfdXVpZCI6IjEyMzQ1Njc4OTAiLCJpc3MiOiJodHRwczovL2FwcGxpY2F0aW9uLm15YXV0aC5jb20iLCJleHAiOjY5NzQwMzE2MDAsImdpdmVuX25hbWUiOiJqb2huIiwiZW1haWwiOiJqb2huLmRvZUBlbWFpbC5vcmciLCJjaWQiOiJUMDAwMzEwIn0.Svrb5PriAuHOdhTFXiicr_qizHiby6b73SdovJAFnWCPDr0r8mmFoEWXjJmdLdw08daNzt8ww_r2khJ-rusUZVfiZY3kyRV1hfeChpNROGfmGbfN62KSsYBPi4dBMIGRz8SqkF6nw5nTC-HOr7Gd8mtZjG9KZYC5fKYOYRvbAZN_xyvLDzFUE6LgLmiT6fV7fHPQi5NSUfawpWQbIgK2sJjnp-ODTAijohyxQNuF4Lq1Prqzjt2QZRwvbskTcYM3gK5fgt6RYDN6MbARJIVFsb1Y7wZFg00dp2XhdFzwWoQl6BluvUL8bL73A8iJSam0csm1cuG0A7kMF9spy_whQw");
 		iasToken = new SapIdToken(IOUtils.resourceToString("/iasOidcTokenRSA256.txt", UTF_8));
 
-		OAuth2ServiceConfiguration mockConfiguration = Mockito.mock(OAuth2ServiceConfiguration.class);
+		mockConfiguration = Mockito.mock(OAuth2ServiceConfiguration.class);
 		when(mockConfiguration.getService()).thenReturn(Service.IAS);
 
 		tokenKeyServiceMock = Mockito.mock(OAuth2TokenKeyService.class);
@@ -101,6 +101,13 @@ public class JwtSignatureValidatorTest {
 				startsWith("Error occurred during signature validation: OIDC token must provide zone_uuid."));
 	}
 
+	@Test
+	public void validationFails_WhenZoneIdIsNull_ButIssuerMatchesOAuth2Url() {
+		when(mockConfiguration.getUrl()).thenReturn(URI.create("https://application.myauth.com"));
+		ValidationResult validationResult = cut.validate(iasPaasToken);
+		assertTrue(validationResult.isValid());
+	}
+
 	@Test
 	public void validate() {
 		assertTrue(cut.validate(iasToken).isValid());
diff --git a/java-security/src/test/resources/iasJsonWebTokenKeys.json b/java-security/src/test/resources/iasJsonWebTokenKeys.json
index fd40eb79..b627fab5 100644
--- a/java-security/src/test/resources/iasJsonWebTokenKeys.json
+++ b/java-security/src/test/resources/iasJsonWebTokenKeys.json
@@ -6,6 +6,13 @@
       "e": "AQAB",
       "use": "sig",
       "n": "AJtUGmczI7RHx3Ypqxz9_9mK_tc-vOXojlJcMm0VRvYvMLIDlIfj1BrkC_IYLpS2Vl1OTG8AS0xAgBDEG3EUzVU6JZKuIuuxD-iXrBySBQA2ytTYtCrjHD7osji7wyogxDJ2BtVz9191gjX7AlU_WKFPpViK2a_2bCL0K4vI3M6-EZMp20wbD2gDsoD1JYqag66WnTDtZqJjQm3mv6Ohj59_C8RMOtPSLX4AxoS-n_8lYneaRc2UFm_vZepgricMNIZ4TuoLekb_fDlg7cvRtH61gD8hH7iFvQfpkf9rxoclPSG21qbxV4svUVW27DOd_Ewo3eSRdnSb8ctuGnXQuKE="
+    },
+    {
+      "kty": "RSA",
+      "kid": "default-kid",
+      "e": "AQAB",
+      "use": "sig",
+      "n": "AJtUGmczI7RHx3Ypqxz9_9mK_tc-vOXojlJcMm0VRvYvMLIDlIfj1BrkC_IYLpS2Vl1OTG8AS0xAgBDEG3EUzVU6JZKuIuuxD-iXrBySBQA2ytTYtCrjHD7osji7wyogxDJ2BtVz9191gjX7AlU_WKFPpViK2a_2bCL0K4vI3M6-EZMp20wbD2gDsoD1JYqag66WnTDtZqJjQm3mv6Ohj59_C8RMOtPSLX4AxoS-n_8lYneaRc2UFm_vZepgricMNIZ4TuoLekb_fDlg7cvRtH61gD8hH7iFvQfpkf9rxoclPSG21qbxV4svUVW27DOd_Ewo3eSRdnSb8ctuGnXQuKE="
     }
   ]
 }
\ No newline at end of file