Repository: SAP/cloud-security-xsuaa-integration
Pull request #108 (2019-07-12 10:34:08)

Integrate xsuaa
Required changes to integrate this lib into xsuaa provisioning.

Changes:
  - Add PostValidationAction Interface, which is executed after succesfull validaiton.
    Required to set IdentyZone.
  - Make it possible to inherit from JwtDecoder/Builder. Enables us to move special xsuua logic into xsuaa
    but reuse the validaiton logic.
  - Add Oauth2AthenticationConverter. Creates authenticaiton of type OAuth2Authentication which is 
    expected from uaa.                           

firnkes: Hi Nena, thanks for your review.

I need to sublcass XsuaajwtDecoder in the xsuaa to implement the special uaa logic there. The idea is to use decode() as a template method and override the getJku/getKid/getUaadomain functions. Xsuaa must change the jku to make the integration tests work. Also it has to detect which kind of token it is (cfuaa/xsuaa) and then either return the uaadomain of the cfuaa or xsuaa. In the case of XSA the jku and kid are not present, so it must build them.

I'll remove the AbstractAuthenticaitonConverter, wrongly thought Oauth2AuthenticationConverter cannot extend TokenAuthenticationConverter. 

It would also be possible to implement nearly all cahnges (beside make XsuaajwtDecoder inheritable) to xsuaa.
However, i think that both other changes: adding a postvalidationaction and an OAuth2AuthenticationConverter could be useful for others too. If you do not see it that way I'll move it into the xsuaa, otherwise I'll adapt the change according to your review.

nenaraab: As discussed, please introduce an interface e.g. `TokenInfoExtractor` with the new methods `getJku`, `getUaaDomain` and `getKid` and inject this via a setter into `XsuaaJwtDecoder`

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe589e11..a1582db6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,9 @@
 # Change Log 
 
 All notable changes to this project will be documented in this file.
-
+## 1.6.0
+* Add OAuth2AuthenticationConverter that can be used to set an authentication object of type OAuth2Authentication.
+* Enables to execute post validation actions that are executed after successful token validation.
 ## 1.5.0
 * Supports `jku` URI which is provided as part of the JSON Web Signature (JWS). The `jku` of the Jwt token header references the public key URI of the Xsuaa OAuth Authorization Server, and needs to match to the `xsuaa.uaadomain`.
 * Completely customizable auto-configurations so that apps can override the spring-xsuaa defaults:
diff --git a/spring-xsuaa/pom.xml b/spring-xsuaa/pom.xml
index e0e0c843..fe433525 100644
--- a/spring-xsuaa/pom.xml
+++ b/spring-xsuaa/pom.xml
@@ -60,6 +60,22 @@
 			<artifactId>caffeine</artifactId>
 			<version>${caffeine.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>org.springframework.security.oauth</groupId>
+			<artifactId>spring-security-oauth2</artifactId>
+			<version>2.3.6.RELEASE</version>
+			<scope>compile</scope>
+			<exclusions>
+				<exclusion>
+					<artifactId>spring-beans</artifactId>
+					<groupId>org.springframework</groupId>
+				</exclusion>
+				<exclusion>
+					<artifactId>spring-context</artifactId>
+					<groupId>org.springframework</groupId>
+				</exclusion>
+			</exclusions>
+		</dependency>
 
 		<dependency>
 			<groupId>junit</groupId>
@@ -97,7 +113,6 @@
 			<version>${project.version}</version>
 			<scope>test</scope>
 		</dependency>
-
 	</dependencies>
 
 	<build>
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaResourceServerJwkAutoConfiguration.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaResourceServerJwkAutoConfiguration.java
index 1a1362c7..2347d9f5 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaResourceServerJwkAutoConfiguration.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/autoconfiguration/XsuaaResourceServerJwkAutoConfiguration.java
@@ -1,7 +1,5 @@
 package com.sap.cloud.security.xsuaa.autoconfiguration;
 
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
-import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder;
 import org.springframework.boot.autoconfigure.AutoConfigureBefore;
 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -14,6 +12,9 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder;
+
 /**
  * {@link EnableAutoConfiguration Auto-configuration} that exposes a
  * {@link JwtDecoder}, which has the standard Spring Security Jwt validators as
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/AuthenticationToken.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/AuthenticationToken.java
index 8142156d..0a2ed2fc 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/AuthenticationToken.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/AuthenticationToken.java
@@ -15,17 +15,25 @@ import org.springframework.security.oauth2.server.resource.authentication.JwtAut
  */
 public class AuthenticationToken extends JwtAuthenticationToken {
 
+	private Token token;
+
 	public AuthenticationToken(Jwt jwt, Collection<GrantedAuthority> authorities) {
 		super(jwt, authorities);
-	}
 
-	@Override
-	public Object getPrincipal() {
 		// Here is where the actual magic happens.
 		// The Jwt is exchanged for another implementation.
 		XsuaaToken token = new XsuaaToken(getToken());
 		token.setAuthorities(this.getAuthorities());
+		this.token = token;
+	}
+
+	@Override
+	public Object getPrincipal() {
 		return token;
 	}
 
+	@Override
+	public String getName() {
+		return token.getUsername();
+	}
 }
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/OAuth2AuthenticationConverter.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/OAuth2AuthenticationConverter.java
new file mode 100644
index 00000000..2f9c5d04
--- /dev/null
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/OAuth2AuthenticationConverter.java
@@ -0,0 +1,34 @@
+package com.sap.cloud.security.xsuaa.token;
+
+import static com.sap.cloud.security.xsuaa.token.TokenClaims.CLAIM_CLIENT_ID;
+
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.provider.AuthorizationRequest;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+
+import com.sap.cloud.security.xsuaa.extractor.AuthoritiesExtractor;
+
+/**
+ * An authentication converter that sets a OAuth2Authentication object. Required
+ * to keep compatibility with UAA.
+ */
+public class OAuth2AuthenticationConverter extends TokenAuthenticationConverter {
+
+	public OAuth2AuthenticationConverter(AuthoritiesExtractor authoritiesExtractor) {
+		super(authoritiesExtractor);
+	}
+
+	@Override
+	public OAuth2Authentication convert(Jwt jwt) {
+		AuthenticationToken authenticationToken = (AuthenticationToken) super.convert(jwt);
+		String clientId = jwt.getClaimAsString(CLAIM_CLIENT_ID);
+		AuthorizationRequest authorizationRequest = new AuthorizationRequest(clientId, null);
+		authorizationRequest.setApproved(true);
+		authorizationRequest.setAuthorities(authenticationToken.getAuthorities());
+
+		return new OAuth2Authentication(authorizationRequest.createOAuth2Request(), authenticationToken);
+	}
+}
\ No newline at end of file
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenClaims.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenClaims.java
index 3393247e..b7de400b 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenClaims.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenClaims.java
@@ -19,4 +19,8 @@ public final class TokenClaims {
 	public static final String CLAIM_GRANT_TYPE = "grant_type";
 	public static final String CLAIM_ZDN = "zdn";
 	public static final String CLAIM_ZONE_ID = "zid";
+	public static final String CLAIM_ISSUER = "iss";
+	public static final String CLAIM_JKU = "jku";
+	public static final String CLAIM_KID = "kid";
+
 }
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/PostValidationAction.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/PostValidationAction.java
new file mode 100644
index 00000000..36b76f76
--- /dev/null
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/PostValidationAction.java
@@ -0,0 +1,8 @@
+package com.sap.cloud.security.xsuaa.token.authentication;
+
+import org.springframework.security.oauth2.jwt.Jwt;
+
+public interface PostValidationAction {
+
+	void perform(Jwt token);
+}
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/TokenInfoExtractor.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/TokenInfoExtractor.java
new file mode 100644
index 00000000..26f5b55b
--- /dev/null
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/TokenInfoExtractor.java
@@ -0,0 +1,16 @@
+package com.sap.cloud.security.xsuaa.token.authentication;
+
+import com.nimbusds.jwt.JWT;
+
+/**
+ * Responsible to extract information out of the token and provide it to the
+ * JwtDecoder.
+ */
+public interface TokenInfoExtractor {
+
+	String getJku(JWT jwt);
+
+	String getKid(JWT jwt);
+
+	String getUaaDomain(JWT jwt);
+}
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
index ee1dba33..fe05bb34 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoder.java
@@ -1,9 +1,13 @@
 package com.sap.cloud.security.xsuaa.token.authentication;
 
+import static com.sap.cloud.security.xsuaa.token.TokenClaims.CLAIM_JKU;
+import static com.sap.cloud.security.xsuaa.token.TokenClaims.CLAIM_KID;
+
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
@@ -27,15 +31,38 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 	private final Logger logger = LoggerFactory.getLogger(getClass());
 
 	Cache<String, JwtDecoder> cache;
-	private String uaaDomain;
 	private OAuth2TokenValidator<Jwt> tokenValidators;
+	private Collection<PostValidationAction> postValidationActions;
+	private TokenInfoExtractor tokenInfoExtractor;
 
 	XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidityInSeconds, int cacheSize,
 			OAuth2TokenValidator<Jwt> tokenValidators) {
 		cache = Caffeine.newBuilder().expireAfterWrite(cacheValidityInSeconds, TimeUnit.SECONDS).maximumSize(cacheSize)
 				.build();
-		this.uaaDomain = xsuaaServiceConfiguration.getUaaDomain();
 		this.tokenValidators = tokenValidators;
+
+		this.tokenInfoExtractor = new TokenInfoExtractor() {
+			@Override
+			public String getJku(JWT jwt) {
+				return (String) jwt.getHeader().toJSONObject().getOrDefault(CLAIM_JKU, null);
+			}
+
+			@Override
+			public String getKid(JWT jwt) {
+				return (String) jwt.getHeader().toJSONObject().getOrDefault(CLAIM_KID, null);
+			}
+
+			@Override
+			public String getUaaDomain(JWT jwt) {
+				return xsuaaServiceConfiguration.getUaaDomain();
+			}
+		};
+	}
+
+	XsuaaJwtDecoder(XsuaaServiceConfiguration xsuaaServiceConfiguration, int cacheValidityInSeconds, int cacheSize,
+			OAuth2TokenValidator<Jwt> tokenValidators, Collection<PostValidationAction> postValidationActions) {
+		this(xsuaaServiceConfiguration, cacheValidityInSeconds, cacheSize, tokenValidators);
+		this.postValidationActions = postValidationActions;
 	}
 
 	@Override
@@ -49,13 +76,19 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 			throw new JwtException("Error initializing JWT decoder: " + ex.getMessage());
 		}
 
-		String jku = (String) jwt.getHeader().toJSONObject().getOrDefault("jku", null);
-		String kid = (String) jwt.getHeader().toJSONObject().getOrDefault("kid", null);
+		String jku = tokenInfoExtractor.getJku(jwt);
+		String kid = tokenInfoExtractor.getKid(jwt);
+		String uaaDomain = tokenInfoExtractor.getUaaDomain(jwt);
 
 		try {
 			canVerifyWithOnlineKey(jku, kid, uaaDomain);
 			validateJKU(jku, uaaDomain);
-			return verifyWithOnlineKey(token, jku, kid);
+			Jwt verifiedToken = verifyWithOnlineKey(token, jku, kid);
+
+			if (postValidationActions != null) {
+				postValidationActions.forEach(act -> act.perform(verifiedToken));
+			}
+			return verifiedToken;
 		} catch (JwtValidationException ex) {
 			throw ex;
 		} catch (JwtException ex) {
@@ -106,4 +139,8 @@ public class XsuaaJwtDecoder implements JwtDecoder {
 		decoder.setJwtValidator(tokenValidators);
 		return decoder;
 	}
+
+	public void setTokenInfoExtractor(TokenInfoExtractor tokenInfoExtractor) {
+		this.tokenInfoExtractor = tokenInfoExtractor;
+	}
 }
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
index f4439fab..cd76f0b2 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaJwtDecoderBuilder.java
@@ -1,5 +1,8 @@
 package com.sap.cloud.security.xsuaa.token.authentication;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
 import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
@@ -14,6 +17,7 @@ public class XsuaaJwtDecoderBuilder {
 	int decoderCacheSize;
 	OAuth2TokenValidator<Jwt> xsuaaTokenValidators;
 	OAuth2TokenValidator<Jwt> defaultTokenValidators;
+	Collection<PostValidationAction> postValidationActions;
 
 	/**
 	 * Utility for building a JWT decoder configuration
@@ -38,7 +42,8 @@ public class XsuaaJwtDecoderBuilder {
 		DelegatingOAuth2TokenValidator<Jwt> combinedTokenValidators = new DelegatingOAuth2TokenValidator<>(
 				defaultTokenValidators,
 				xsuaaTokenValidators);
-		return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize, combinedTokenValidators);
+		return new XsuaaJwtDecoder(configuration, decoderCacheValidity, decoderCacheSize,
+				combinedTokenValidators, postValidationActions);
 	}
 
 	/**
@@ -67,6 +72,19 @@ public class XsuaaJwtDecoderBuilder {
 		return this;
 	}
 
+	/**
+	 * Sets the PostValidationActions that are executed after successful
+	 * verification and validation of the token.
+	 *
+	 * @param postValidationActions
+	 *            the PostValidationActions
+	 * @return this
+	 */
+	public XsuaaJwtDecoderBuilder withPostValidationActions(PostValidationAction... postValidationActions) {
+		this.postValidationActions = Arrays.asList(postValidationActions);
+		return this;
+	}
+
 	/**
 	 * Configures clone token validator, in case of two xsuaa bindings (application
 	 * and broker plan).


Test patch:
diff --git a/spring-xsuaa-it/src/test/java/testservice/api/XsuaaJwtDecoderTest.java b/spring-xsuaa-it/src/test/java/testservice/api/XsuaaJwtDecoderTest.java
new file mode 100644
index 00000000..cf6255af
--- /dev/null
+++ b/spring-xsuaa-it/src/test/java/testservice/api/XsuaaJwtDecoderTest.java
@@ -0,0 +1,73 @@
+package testservice.api;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtException;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.autoconfiguration.XsuaaAutoConfiguration;
+import com.sap.cloud.security.xsuaa.autoconfiguration.XsuaaResourceServerJwkAutoConfiguration;
+import com.sap.cloud.security.xsuaa.test.JwtGenerator;
+import com.sap.cloud.security.xsuaa.token.authentication.XsuaaJwtDecoderBuilder;
+
+import testservice.api.nohttp.MyEventHandler;
+import testservice.api.nohttp.SecurityConfiguration;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = { SecurityConfiguration.class, MyEventHandler.class,
+		XsuaaAutoConfiguration.class,
+		XsuaaResourceServerJwkAutoConfiguration.class })
+@ActiveProfiles({ "test.api.nohttp", "uaamock" })
+public class XsuaaJwtDecoderTest {
+
+	boolean postActionExecuted;
+	JwtDecoder jwtDecoderWithPostAction;
+
+	@Value("${xsuaa.clientid}")
+	String clientId;
+
+	@Value("${xsuaa.xsappname}")
+	String xsappname;
+
+	@Autowired
+	XsuaaServiceConfiguration serviceConfiguration;
+
+	@Autowired
+	MyEventHandler eventHandler;
+
+	@Before
+	public void setUp() {
+		postActionExecuted = false;
+		jwtDecoderWithPostAction = new XsuaaJwtDecoderBuilder(serviceConfiguration)
+				.withPostValidationActions(token -> postActionExecuted = true).build();
+	}
+
+	@Test
+	public void postValidationActionIsExecutedIfSuccess() {
+		String jwt = new JwtGenerator(clientId, "subdomain").deriveAudiences(true)
+				.setJwtHeaderKeyId("legacy-token-key").getToken().getTokenValue();
+
+		jwtDecoderWithPostAction.decode(jwt);
+		Assert.assertTrue(postActionExecuted);
+	}
+
+	@Test
+	public void postValidationActionIsNotExecutedIfFail() {
+		String jwt = new JwtGenerator(clientId, "subdomain").deriveAudiences(true)
+				.setJwtHeaderKeyId("legacy-token-key").setJku(null).getToken().getTokenValue();
+		try {
+			jwtDecoderWithPostAction.decode(jwt);
+			Assert.fail();
+		} catch (JwtException e) {
+			Assert.assertFalse(postActionExecuted);
+		}
+	}
+}
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/OAuth2TokenAuthenticationConverterTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/OAuth2TokenAuthenticationConverterTest.java
new file mode 100644
index 00000000..6c49089d
--- /dev/null
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/OAuth2TokenAuthenticationConverterTest.java
@@ -0,0 +1,42 @@
+package com.sap.cloud.security.xsuaa.token;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+
+import com.sap.cloud.security.xsuaa.extractor.DefaultAuthoritiesExtractor;
+import com.sap.cloud.security.xsuaa.test.JwtGenerator;
+
+public class OAuth2TokenAuthenticationConverterTest {
+	private String xsAppName = "my-app-name!400";
+	private OAuth2AuthenticationConverter tokenConverterOauth2;
+	String scopeAdmin = xsAppName + "." + "Admin";
+	String scopeRead = xsAppName + "." + "Read";
+	String scopeOther = "other-app!234" + "." + "Other";
+
+	@Before
+	public void setup() {
+		tokenConverterOauth2 = new OAuth2AuthenticationConverter(new DefaultAuthoritiesExtractor());
+	}
+
+	@Test
+	public void extractAuthoritiesWithScopesOAuth2Authentication() {
+		Jwt jwt = new JwtGenerator().addScopes(scopeAdmin, scopeRead, scopeOther).getToken();
+
+		AbstractAuthenticationToken authenticationToken = tokenConverterOauth2.convert(jwt);
+		assertThat(authenticationToken.getAuthorities().size(), is(3));
+		assertThat(authenticationToken.getAuthorities(), hasItem(new SimpleGrantedAuthority(scopeRead)));
+		assertThat(authenticationToken.getAuthorities(), hasItem(new SimpleGrantedAuthority(scopeAdmin)));
+		assertThat(authenticationToken.getAuthorities(), hasItem(new SimpleGrantedAuthority(scopeOther)));
+
+		assertTrue(authenticationToken instanceof OAuth2Authentication);
+	}
+}