Repository: SAP/cloud-security-xsuaa-integration
Pull request #431 (2020-12-11 17:03:10)

Ias xsuaa token exchange


Patch:
diff --git a/java-security/pom.xml b/java-security/pom.xml
index 2d88b3c4..470328ba 100644
--- a/java-security/pom.xml
+++ b/java-security/pom.xml
@@ -102,7 +102,13 @@
 			<artifactId>slf4j-simple</artifactId>
 			<scope>test</scope>
 		</dependency>
-    </dependencies>
+		<dependency>
+			<groupId>com.github.stefanbirkner</groupId>
+			<artifactId>system-lambda</artifactId>
+			<version>1.1.1</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
 
 	<build>
 		<plugins>
diff --git a/java-security/src/main/java/com/sap/cloud/security/servlet/AbstractTokenAuthenticator.java b/java-security/src/main/java/com/sap/cloud/security/servlet/AbstractTokenAuthenticator.java
index 873dd4d2..fb19b5fb 100644
--- a/java-security/src/main/java/com/sap/cloud/security/servlet/AbstractTokenAuthenticator.java
+++ b/java-security/src/main/java/com/sap/cloud/security/servlet/AbstractTokenAuthenticator.java
@@ -40,13 +40,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 			if (headerIsAvailable(authorizationHeader)) {
 				try {
 					Token token = extractFromHeader(authorizationHeader);
-					ValidationResult result = getOrCreateTokenValidator().validate(token);
-					if (result.isValid()) {
-						SecurityContext.setToken(token);
-						return authenticated(token);
-					} else {
-						return unauthenticated("Error during token validation: " + result.getErrorDescription());
-					}
+					return tokenValidationResult(token);
 				} catch (Exception e) {
 					return unauthenticated("Unexpected error occurred: " + e.getMessage());
 				}
@@ -59,7 +53,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 
 	/**
 	 * Use to configure the token key cache.
-	 * 
+	 *
 	 * @param cacheConfiguration
 	 *            the cache configuration
 	 * @return this authenticator
@@ -71,7 +65,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 
 	/**
 	 * Use to configure the HttpClient that is used to retrieve token keys.
-	 * 
+	 *
 	 * @param httpClient
 	 *            the HttpClient
 	 * @return this authenticator
@@ -83,7 +77,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 
 	/**
 	 * Use to override the service configuration used.
-	 * 
+	 *
 	 * @param serviceConfiguration
 	 *            the service configuration to use
 	 * @return this authenticator
@@ -96,7 +90,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 	/**
 	 * Adds the validation listener to the jwt validator that is being used by the
 	 * authenticator to validate the tokens.
-	 * 
+	 *
 	 * @param validationListener
 	 *            the listener to be added.
 	 * @return the authenticator instance
@@ -109,7 +103,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 	/**
 	 * Return configured service configuration or Environments.getCurrent() if not
 	 * configured.
-	 * 
+	 *
 	 * @return the actual service configuration
 	 * @throws IllegalStateException
 	 *             in case service configuration is null
@@ -133,7 +127,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 	 */
 	protected abstract Token extractFromHeader(String authorizationHeader);
 
-	private Validator<Token> getOrCreateTokenValidator() {
+	Validator<Token> getOrCreateTokenValidator() {
 		if (tokenValidator == null) {
 			JwtValidatorBuilder jwtValidatorBuilder = JwtValidatorBuilder.getInstance(getServiceConfiguration())
 					.withHttpClient(httpClient);
@@ -145,7 +139,7 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 		return tokenValidator;
 	}
 
-	private TokenAuthenticationResult unauthenticated(String message) {
+	TokenAuthenticationResult unauthenticated(String message) {
 		logger.warn("Request could not be authenticated: {}.", message);
 		return TokenAuthenticatorResult.createUnauthenticated(message);
 	}
@@ -154,8 +148,19 @@ public abstract class AbstractTokenAuthenticator implements TokenAuthenticator {
 		return TokenAuthenticatorResult.createAuthenticated(Collections.emptyList(), token);
 	}
 
-	private boolean headerIsAvailable(String authorizationHeader) {
+	boolean headerIsAvailable(String authorizationHeader) {
 		return authorizationHeader != null && !authorizationHeader.isEmpty();
 	}
 
+	TokenAuthenticationResult tokenValidationResult(Token token) {
+		Validator<Token> validator = getOrCreateTokenValidator();
+		ValidationResult result = validator.validate(token);
+		if (result.isValid()) {
+			SecurityContext.setToken(token);
+			return authenticated(token);
+		} else {
+			return unauthenticated("Error during token validation: " + result.getErrorDescription());
+		}
+	}
+
 }
diff --git a/java-security/src/main/java/com/sap/cloud/security/servlet/IasXsuaaExchangeBroker.java b/java-security/src/main/java/com/sap/cloud/security/servlet/IasXsuaaExchangeBroker.java
new file mode 100644
index 00000000..02ef210f
--- /dev/null
+++ b/java-security/src/main/java/com/sap/cloud/security/servlet/IasXsuaaExchangeBroker.java
@@ -0,0 +1,48 @@
+package com.sap.cloud.security.servlet;
+
+import com.sap.cloud.security.config.OAuth2ServiceConfiguration;
+import com.sap.cloud.security.token.Token;
+import com.sap.cloud.security.xsuaa.client.ClientCredentials;
+import com.sap.cloud.security.xsuaa.client.DefaultOAuth2TokenService;
+import com.sap.cloud.security.xsuaa.client.OAuth2TokenResponse;
+import com.sap.cloud.security.xsuaa.client.XsuaaDefaultEndpoints;
+import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
+import com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlows;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+
+/**
+ * IAS token and XSUAA token exchange class. Facilitates token exchange between
+ * IAS and XSUAA
+ */
+public class IasXsuaaExchangeBroker {
+
+	private static final Logger logger = LoggerFactory.getLogger(IasXsuaaExchangeBroker.class);
+
+	/**
+	 * Request a Xsuaa token using Ias token as a grant.
+	 *
+	 * @param httpClient
+	 *            http client that will perform the request
+	 * @param token
+	 *            decoded IAS token
+	 * @param serviceConfiguration
+	 *            Xsuaa service configuration
+	 * @return encoded Xsuaa token
+	 */
+	@Nullable
+	public String doIasToXsuaaXchange(CloseableHttpClient httpClient, Token token,
+			OAuth2ServiceConfiguration serviceConfiguration) throws TokenFlowException {
+		XsuaaTokenFlows tokenFlows = new XsuaaTokenFlows(
+				new DefaultOAuth2TokenService(httpClient),
+				new XsuaaDefaultEndpoints(serviceConfiguration.getUrl()),
+				new ClientCredentials(serviceConfiguration.getClientId(), serviceConfiguration.getClientSecret()));
+		OAuth2TokenResponse tokenResponse = tokenFlows.userTokenFlow().token(token.getTokenValue()).execute();
+		logger.debug("Response token from Ias to Xsuaa token exchange {}", tokenResponse.getAccessToken());
+		return tokenResponse.getAccessToken();
+	}
+
+}
diff --git a/java-security/src/main/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticator.java b/java-security/src/main/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticator.java
index ce4a2ede..b3be6ae8 100644
--- a/java-security/src/main/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticator.java
+++ b/java-security/src/main/java/com/sap/cloud/security/servlet/XsuaaTokenAuthenticator.java
@@ -3,13 +3,39 @@ package com.sap.cloud.security.servlet;
 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.json.JsonObject;
+import com.sap.cloud.security.token.ScopeConverter;
+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.xsuaa.http.HttpHeaders;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.annotation.Nullable;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import java.util.Collection;
+import java.util.Objects;
+
+import static com.sap.cloud.security.token.TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE;
+import static com.sap.cloud.security.token.TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE_ENHANCER;
 
 public class XsuaaTokenAuthenticator extends AbstractTokenAuthenticator {
 
+	private static final Logger LOGGER = LoggerFactory.getLogger(XsuaaTokenAuthenticator.class);
+	private static final String IAS_XSUAA_ENABLED = "IAS_XSUAA_XCHANGE_ENABLED";
+	private IasXsuaaExchangeBroker exchangeBroker;
+
+	public XsuaaTokenAuthenticator() {
+	}
+
+	public XsuaaTokenAuthenticator(IasXsuaaExchangeBroker exchangeBroker) {
+		this.exchangeBroker = exchangeBroker;
+	}
+
 	@Override
 	public Token extractFromHeader(String authorizationHeader) {
 		return new XsuaaToken(authorizationHeader)
@@ -39,9 +65,71 @@ public class XsuaaTokenAuthenticator extends AbstractTokenAuthenticator {
 		return TokenAuthenticatorResult.createAuthenticated(translatedScopes, token);
 	}
 
+	@Override
+	public TokenAuthenticationResult validateRequest(ServletRequest request, ServletResponse response) {
+		if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+			HttpServletRequest httpRequest = (HttpServletRequest) request;
+			String authorizationHeader = httpRequest.getHeader(HttpHeaders.AUTHORIZATION);
+			if (headerIsAvailable(authorizationHeader)) {
+				try {
+					Token token = extractFromHeader(authorizationHeader);
+					if (isIasXsuaaXchangeEnabled() && !isXsuaaToken(token)) {
+						token = new XsuaaToken(Objects.requireNonNull(
+								exchangeBroker.doIasToXsuaaXchange(httpClient, token, serviceConfiguration),
+								"IasXsuaaExchangeBroker is not provided"));
+					}
+					return tokenValidationResult(token);
+				} catch (Exception e) {
+					return unauthenticated("Unexpected error occurred: " + e.getMessage());
+				}
+			} else {
+				return unauthenticated("Authorization header is missing.");
+			}
+		}
+		return TokenAuthenticatorResult.createUnauthenticated("Could not process request " + request);
+	}
+
 	private ScopeConverter getScopeConverter() {
 		return new XsuaaScopeConverter(
 				getServiceConfiguration().getProperty(CFConstants.XSUAA.APP_ID));
 	}
 
+	/**
+	 * Verifies if the provided token is Xsuaa token
+	 *
+	 * @exception NullPointerException
+	 *                if the external attribute enhancer(enhancer) value is missing
+	 *                but the external attribute(ext_atr) claim is present
+	 * @param token
+	 *            Token to be checked
+	 * @return true if provided token is a XSUAA token
+	 */
+	private boolean isXsuaaToken(Token token) {
+		JsonObject externalAttributeClaim = token.getClaimAsJsonObject(EXTERNAL_ATTRIBUTE);
+
+		if (externalAttributeClaim != null) {
+			String externalAttributeValue = externalAttributeClaim.getAsString(EXTERNAL_ATTRIBUTE_ENHANCER);
+			LOGGER.debug("Token attributes claims: {}: {}", EXTERNAL_ATTRIBUTE, externalAttributeClaim);
+			return Objects.requireNonNull(externalAttributeValue, "enhancer is missing").equalsIgnoreCase("xsuaa");
+		} else {
+			LOGGER.debug("Token is not Xsuaa token: {}", token.getTokenValue());
+			return false;
+		}
+	}
+
+	/**
+	 * Checks value of environment variable 'IAS_XSUAA_XCHANGE_ENABLED'. This value
+	 * determines, whether token exchange between IAS and XSUAA is enabled. If
+	 * IAS_XSUAA_XCHANGE_ENABLED is not provided or with an empty value or with
+	 * value = false, then token exchange is disabled. Any other values are
+	 * interpreted as true.
+	 *
+	 * @return returns true if exchange is enabled and false if disabled
+	 */
+
+	private boolean isIasXsuaaXchangeEnabled() {
+		String isEnabled = System.getenv(IAS_XSUAA_ENABLED);
+		LOGGER.debug("System environment variable {} is set to {}", IAS_XSUAA_ENABLED, isEnabled);
+		return isEnabled != null && !isEnabled.equalsIgnoreCase("false");
+	}
 }
diff --git a/samples/java-security-usage/manifest.yml b/samples/java-security-usage/manifest.yml
index 2844d9b2..5e0107f6 100644
--- a/samples/java-security-usage/manifest.yml
+++ b/samples/java-security-usage/manifest.yml
@@ -14,4 +14,6 @@ applications:
     - java_buildpack
   services:
     - xsuaa-java-security
+  env:
+    IAS_XSUAA_XCHANGE_ENABLED: true
 ...


Test patch:
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 cb7c4ac6..c2cb1218 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
@@ -4,11 +4,13 @@ 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.config.cf.CFConstants;
+import com.sap.cloud.security.token.SapIdToken;
 import com.sap.cloud.security.token.SecurityContext;
 import com.sap.cloud.security.token.XsuaaToken;
 import com.sap.cloud.security.token.validation.ValidationListener;
 import com.sap.cloud.security.util.HttpClientTestFactory;
 import com.sap.cloud.security.xsuaa.http.HttpHeaders;
+import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
 import org.apache.commons.io.IOUtils;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
@@ -21,10 +23,13 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 
+import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.when;
 
 public class XsuaaTokenAuthenticatorTest {
 
@@ -32,7 +37,9 @@ public class XsuaaTokenAuthenticatorTest {
 
 	private final XsuaaToken xsuaaToken;
 	private final XsuaaToken invalidSignatureToken;
+	private final SapIdToken iasToken;
 	private CloseableHttpClient mockHttpClient;
+	private OAuth2ServiceConfiguration oAuth2ServiceConfiguration;
 
 	private AbstractTokenAuthenticator cut;
 
@@ -40,6 +47,7 @@ public class XsuaaTokenAuthenticatorTest {
 		xsuaaToken = new XsuaaToken(IOUtils.resourceToString("/xsuaaUserAccessTokenRSA256.txt", UTF_8));
 		invalidSignatureToken = new XsuaaToken(
 				IOUtils.resourceToString("/xsuaaInvalidSignatureTokenRSA256.txt", UTF_8));
+		iasToken = new SapIdToken(IOUtils.resourceToString("/iasOidcTokenRSA256.txt", UTF_8));
 	}
 
 	@Before
@@ -50,11 +58,13 @@ public class XsuaaTokenAuthenticatorTest {
 				.createHttpResponse(IOUtils.resourceToString("/jsonWebTokenKeys.json", UTF_8));
 		when(mockHttpClient.execute(any(HttpGet.class))).thenReturn(response);
 
-		OAuth2ServiceConfiguration oAuth2ServiceConfiguration = OAuth2ServiceConfigurationBuilder
+		oAuth2ServiceConfiguration = OAuth2ServiceConfigurationBuilder
 				.forService(Service.XSUAA)
 				.withProperty(CFConstants.XSUAA.UAA_DOMAIN, "auth.com")
 				.withProperty(CFConstants.XSUAA.APP_ID, "appId")
 				.withClientId("clientId")
+				.withClientSecret("mySecret")
+				.withUrl("https://myauth.com")
 				.build();
 
 		cut = new XsuaaTokenAuthenticator()
@@ -138,6 +148,104 @@ public class XsuaaTokenAuthenticatorTest {
 		Mockito.verifyNoMoreInteractions(validationListener2);
 	}
 
+	@Test
+	public void validateRequest_tokenXchangeEnabled_IasToken() throws Exception {
+		IasXsuaaExchangeBroker mockExchangeBroker = Mockito.mock(IasXsuaaExchangeBroker.class);
+		when(mockExchangeBroker.doIasToXsuaaXchange(any(), any(), any())).thenReturn(xsuaaToken.getTokenValue());
+
+		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
+				.withHttpClient(mockHttpClient)
+				.withServiceConfiguration(oAuth2ServiceConfiguration);
+
+		HttpServletRequest httpRequest = createRequestWithToken(iasToken.getTokenValue());
+		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
+		withEnvironmentVariable("IAS_XSUAA_XCHANGE_ENABLED", "true")
+				.execute(() -> response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE));
+		assertEquals(response[0].getToken(), xsuaaToken);
+
+	}
+
+	@Test
+	public void validateRequest_tokenXchangeEnabled_IasTokenXchangeBrokerError() throws Exception {
+		IasXsuaaExchangeBroker mockExchangeBroker = Mockito.mock(IasXsuaaExchangeBroker.class);
+		when(mockExchangeBroker.doIasToXsuaaXchange(any(), any(), any())).thenThrow(TokenFlowException.class);
+
+		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
+				.withHttpClient(mockHttpClient)
+				.withServiceConfiguration(oAuth2ServiceConfiguration);
+
+		HttpServletRequest httpRequest = createRequestWithToken(iasToken.getTokenValue());
+		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
+		withEnvironmentVariable("IAS_XSUAA_XCHANGE_ENABLED", "true")
+				.execute(() -> response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE));
+		assertFalse(response[0].isAuthenticated());
+	}
+
+	@Test
+	public void validateRequest_tokenXchangeEnabled_validXsuaa() throws Exception {
+		IasXsuaaExchangeBroker mockExchangeBroker = Mockito.mock(IasXsuaaExchangeBroker.class);
+		when(mockExchangeBroker.doIasToXsuaaXchange(any(), any(), any())).thenReturn(xsuaaToken.getTokenValue());
+
+		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
+				.withHttpClient(mockHttpClient)
+				.withServiceConfiguration(oAuth2ServiceConfiguration);
+
+		HttpServletRequest httpRequest = createRequestWithToken(xsuaaToken.getTokenValue());
+		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
+		withEnvironmentVariable("IAS_XSUAA_XCHANGE_ENABLED", "true")
+				.execute(() -> response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE));
+		assertEquals(response[0].getToken(), xsuaaToken);
+	}
+
+	@Test
+	public void validateRequest_tokenXchangeEnabled_invalidXsuaa() throws Exception {
+		IasXsuaaExchangeBroker mockExchangeBroker = Mockito.mock(IasXsuaaExchangeBroker.class);
+		when(mockExchangeBroker.doIasToXsuaaXchange(any(), any(), any())).thenThrow(TokenFlowException.class);
+
+		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
+				.withHttpClient(mockHttpClient)
+				.withServiceConfiguration(oAuth2ServiceConfiguration);
+
+		HttpServletRequest httpRequest = createRequestWithToken(invalidSignatureToken.getTokenValue());
+		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
+		withEnvironmentVariable("IAS_XSUAA_XCHANGE_ENABLED", "true")
+				.execute(() -> response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE));
+		assertFalse(response[0].isAuthenticated());
+	}
+
+	@Test
+	public void validateRequest_tokenXchangeFalse_validXsuaa() throws Exception {
+
+		IasXsuaaExchangeBroker mockExchangeBroker = Mockito.mock(IasXsuaaExchangeBroker.class);
+		when(mockExchangeBroker.doIasToXsuaaXchange(any(), any(), any())).thenReturn(xsuaaToken.getTokenValue());
+
+		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
+				.withHttpClient(mockHttpClient)
+				.withServiceConfiguration(oAuth2ServiceConfiguration);
+
+		HttpServletRequest httpRequest = createRequestWithToken(xsuaaToken.getTokenValue());
+		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
+		withEnvironmentVariable("IAS_XSUAA_XCHANGE_ENABLED", "false")
+				.execute(() -> response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE));
+		assertTrue(response[0].isAuthenticated());
+	}
+
+	@Test
+	public void validateRequest_tokenXchangeFalse_invalidXsuaa() throws Exception {
+		IasXsuaaExchangeBroker mockExchangeBroker = Mockito.mock(IasXsuaaExchangeBroker.class);
+		when(mockExchangeBroker.doIasToXsuaaXchange(any(), any(), any())).thenThrow(TokenFlowException.class);
+
+		cut = new XsuaaTokenAuthenticator(mockExchangeBroker)
+				.withHttpClient(mockHttpClient)
+				.withServiceConfiguration(oAuth2ServiceConfiguration);
+
+		HttpServletRequest httpRequest = createRequestWithToken(invalidSignatureToken.getTokenValue());
+		final TokenAuthenticationResult[] response = new TokenAuthenticationResult[1];
+		withEnvironmentVariable("IAS_XSUAA_XCHANGE_ENABLED", "false")
+				.execute(() -> response[0] = cut.validateRequest(httpRequest, HTTP_RESPONSE));
+		assertFalse(response[0].isAuthenticated());
+	}
+
 	private HttpServletRequest createRequestWithoutToken() {
 		return Mockito.mock(HttpServletRequest.class);
 	}