Repository: SAP/cloud-security-xsuaa-integration
Pull request #69 (2019-04-19 10:22:43)

new method: Token.getExpirationDate()
closes #https://github.com/SAP/cloud-security-xsuaa-integration/issues/68

nenaraab: @mwdb 
can you please perform a review?

Patch:
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java
index 9a0da6b6..07a477f9 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/extractor/TokenBrokerResolver.java
@@ -17,6 +17,7 @@ import org.springframework.cache.Cache;
 import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
 
 import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import org.springframework.util.StringUtils;
 
 /**
  * Analyse authentication header and obtain token from UAA
@@ -105,7 +106,9 @@ public class TokenBrokerResolver implements BearerTokenResolver {
 		for (AuthenticationMethod credentialType : authenticationMethods) {
 			Enumeration<String> headers = request.getHeaders(AUTHORIZATION_HEADER);
 			String token = getBrokerToken(credentialType, headers, oauthTokenUrl);
-			return token;
+			if (!StringUtils.isEmpty(token)) {
+				return token;
+			}
 		}
 		return null;
 	}
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java
index 206a289c..3a1fa3d0 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/Token.java
@@ -7,6 +7,7 @@ import org.springframework.security.core.userdetails.UserDetails;
 
 import java.net.URISyntaxException;
 import java.util.Collection;
+import java.util.Date;
 
 public interface Token extends UserDetails {
 	String CLAIM_XS_USER_ATTRIBUTES = "xs.user.attributes";
@@ -141,7 +142,7 @@ public interface Token extends UserDetails {
 
 	/**
 	 * Returns list of scopes with appId prefix, e.g.
-	 * "&lt;my-xsapp!123&gt;.Display".
+	 * "&lt;my-app!t123&gt;.Display".
 	 *
 	 * @return all scopes
 	 */
@@ -157,4 +158,11 @@ public interface Token extends UserDetails {
 	 */
 	@Override
 	Collection<? extends GrantedAuthority> getAuthorities();
+
+	/**
+	 * Returns date of when jwt token expires.
+	 *
+	 * @return expiration date
+	 */
+	Date getExpirationDate();
 }
\ No newline at end of file
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java
index ea0f3947..b05c4ed9 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/TokenImpl.java
@@ -3,6 +3,7 @@ package com.sap.cloud.security.xsuaa.token;
 import java.net.URISyntaxException;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 
@@ -54,7 +55,7 @@ public class TokenImpl implements Token {
 	 * @param jwt
 	 *            token
 	 * @param appId
-	 *            app name
+	 *            e.g. myapp!t123
 	 */
 	protected TokenImpl(Jwt jwt, String appId) {
 		this.appId = appId;
@@ -66,6 +67,10 @@ public class TokenImpl implements Token {
 		return this.authorities;
 	}
 
+	@Override public Date getExpirationDate() {
+		return jwt.getExpiresAt() != null ? Date.from(jwt.getExpiresAt()) : null;
+	}
+
 	@Override
 	public String getPassword() {
 		return null;
diff --git a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java
index 9f9dc55b..e347a4a7 100644
--- a/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java
+++ b/spring-xsuaa/src/main/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidator.java
@@ -2,16 +2,16 @@ package com.sap.cloud.security.xsuaa.token.authentication;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
+import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
+import com.sap.cloud.security.xsuaa.token.Token;
 import org.springframework.security.oauth2.core.OAuth2Error;
 import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
 import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
 import org.springframework.security.oauth2.jwt.Jwt;
 
-import com.sap.cloud.security.xsuaa.XsuaaServiceConfiguration;
-import com.sap.cloud.security.xsuaa.token.Token;
-
 /**
  * Validate audience using audience field content. in case this field is empty,
  * the audience is derived from the scope field
@@ -34,7 +34,7 @@ public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
 			return OAuth2TokenValidatorResult.success();
 		} else {
 			// case 2: foreign token
-			List<String> allowedAudiences = allowedAudiences(token);
+			List<String> allowedAudiences = getAllowedAudiences(token);
 			if (allowedAudiences.contains(xsuaaServiceConfiguration.getAppId())) {
 				return OAuth2TokenValidatorResult.success();
 			} else {
@@ -51,7 +51,7 @@ public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
 	 * @param token
 	 * @return (empty) list of audiences
 	 */
-	private List<String> allowedAudiences(Jwt token) {
+	List<String> getAllowedAudiences(Jwt token) {
 		List<String> allAudiences = new ArrayList<>();
 		List<String> tokenAudiences = token.getAudience();
 
@@ -75,7 +75,7 @@ public class XsuaaAudienceValidator implements OAuth2TokenValidator<Jwt> {
 				}
 			}
 		}
-		return allAudiences;
+		return allAudiences.stream().distinct().filter(value -> !value.isEmpty()).collect(Collectors.toList());
 	}
 
 	private List<String> getScopes(Jwt token) {


Test patch:
diff --git a/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/BasicCredentialExtractorTest.java b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/BasicCredentialExtractorTest.java
index 44177c4d..fd8a31cb 100644
--- a/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/BasicCredentialExtractorTest.java
+++ b/spring-xsuaa-it/src/test/java/com/sap/cloud/security/xsuaa/extractor/BasicCredentialExtractorTest.java
@@ -40,20 +40,6 @@ public class BasicCredentialExtractorTest {
 	@Autowired
 	private AuthenticationInformationExtractor authenticationConfiguration;
 
-	/**
-	 * @throws java.lang.Exception
-	 */
-	@BeforeClass
-	public static void setUpBeforeClass() throws Exception {
-	}
-
-	/**
-	 * @throws java.lang.Exception
-	 */
-	@AfterClass
-	public static void tearDownAfterClass() throws Exception {
-	}
-
 	/**
 	 * @throws java.lang.Exception
 	 */
@@ -62,13 +48,6 @@ public class BasicCredentialExtractorTest {
 		request = new MockHttpServletRequest();
 	}
 
-	/**
-	 * @throws java.lang.Exception
-	 */
-	@After
-	public void tearDown() throws Exception {
-	}
-
 	@Test
 	public void testBasicCredentialsNoMultiTenancy() {
 		TokenBrokerResolver extractor = new TokenBrokerResolver(getXsuaaServiceConfiguration(), tokenCache, tokenBroker,
@@ -90,27 +69,6 @@ public class BasicCredentialExtractorTest {
 		assertThat(token).isEqualTo("other_token_pwd");
 	}
 
-	private XsuaaServiceConfigurationDummy getXsuaaServiceConfiguration() {
-		XsuaaServiceConfigurationDummy cfg = new XsuaaServiceConfigurationDummy();
-		cfg.appId = "a1!123";
-		cfg.clientId = "myclient!t1";
-		cfg.clientSecret = "top.secret";
-		cfg.uaaDomain = "auth.com";
-		cfg.uaaUrl = "https://mydomain.auth.com";
-		return cfg;
-	}
-
-	public AuthenticationInformationExtractor authenticationMethods(AuthenticationMethod... methods) {
-		return new DefaultAuthenticationInformationExtractor() {
-
-			@Override
-			public List<AuthenticationMethod> getAuthenticationMethods(HttpServletRequest request) {
-				return Arrays.asList(methods);
-			}
-		};
-
-	}
-
 	@Test
 	public void testClientCredentials() {
 		TokenBrokerResolver extractor = new TokenBrokerResolver(getXsuaaServiceConfiguration(), tokenCache, tokenBroker,
@@ -137,7 +95,7 @@ public class BasicCredentialExtractorTest {
 	}
 
 	@Test(expected = IllegalArgumentException.class)
-	public void invalidCombinedCredentials() {
+	public void testInvalidCombinedCredentials() {
 
 		AuthenticationInformationExtractor invalidCombination = new DefaultAuthenticationInformationExtractor() {
 
@@ -156,16 +114,47 @@ public class BasicCredentialExtractorTest {
 	}
 
 	@Test
-	public void combinedCredentials() {
+	public void testCombinedCredentials() {
 		request.addHeader("Authorization", "Bearer "
 				+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c");
 
 		TokenBrokerResolver extractor = new TokenBrokerResolver(getXsuaaServiceConfiguration(), tokenCache, tokenBroker,
-				authenticationMethods(AuthenticationMethod.OAUTH2, AuthenticationMethod.OAUTH2));
+				authenticationMethods(AuthenticationMethod.OAUTH2, AuthenticationMethod.BASIC));
 
 		String token = extractor.resolve(request);
 		assertThat(token).isEqualTo(
 				"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c");
 	}
 
+	@Test
+	public void testCombinedCredentials_shouldTakeBasicAsFallback() {
+		request.addHeader("Authorization", "basic " + Base64.getEncoder().encodeToString("myuser:mypass".getBytes()));
+
+		TokenBrokerResolver extractor = new TokenBrokerResolver(getXsuaaServiceConfiguration(), tokenCache, tokenBroker,
+				authenticationMethods(AuthenticationMethod.BASIC, AuthenticationMethod.OAUTH2));
+
+		String token = extractor.resolve(request);
+		assertThat(token).isEqualTo("token_pwd");
+	}
+
+	private XsuaaServiceConfigurationDummy getXsuaaServiceConfiguration() {
+		XsuaaServiceConfigurationDummy cfg = new XsuaaServiceConfigurationDummy();
+		cfg.appId = "a1!123";
+		cfg.clientId = "myclient!t1";
+		cfg.clientSecret = "top.secret";
+		cfg.uaaDomain = "auth.com";
+		cfg.uaaUrl = "https://mydomain.auth.com";
+		return cfg;
+	}
+
+	public AuthenticationInformationExtractor authenticationMethods(AuthenticationMethod... methods) {
+		return new DefaultAuthenticationInformationExtractor() {
+
+			@Override
+			public List<AuthenticationMethod> getAuthenticationMethods(HttpServletRequest request) {
+				return Arrays.asList(methods);
+			}
+		};
+
+	}
 }
diff --git a/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java
index f77748f3..e62055bf 100644
--- a/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java
+++ b/spring-xsuaa-test/src/main/java/com/sap/cloud/security/xsuaa/test/JwtGenerator.java
@@ -198,9 +198,10 @@ public class JwtGenerator {
 		for (String scope : scopes) {
 			if (scope.contains(".")) {
 				String aud = scope.substring(0, scope.indexOf("."));
-				if (!audiences.contains(aud)) {
-					audiences.add(aud);
+				if (aud.isEmpty() || audiences.contains(aud)) {
+					break;
 				}
+				audiences.add(aud);
 			}
 		}
 		return audiences;
diff --git a/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java b/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java
index cbb6ac6a..86ac06c9 100644
--- a/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java
+++ b/spring-xsuaa-test/src/test/java/com/sap/cloud/security/xsuaa/test/JwtGeneratorTest.java
@@ -109,7 +109,7 @@ public class JwtGeneratorTest {
 	}
 
 	@Test
-	public void testTokenWithCustomClaims() throws IOException {
+	public void testTokenWithCustomClaims() {
 		Map<String, Object> claims = new HashMap<>();
 		claims.put("myCustomClaim", "myCustomClaimValue");
 		Jwt jwt = jwtGenerator.addCustomClaims(claims).getToken();
@@ -118,9 +118,9 @@ public class JwtGeneratorTest {
 	}
 
 	@Test
-	public void testTokenWithAudienceClaims() throws IOException {
+	public void testTokenWithDerivedAudienceClaim() {
 
-		Jwt jwt = jwtGenerator.addScopes("openid", "app1.scope", "app2.sub.scope", "app2.scope").deriveAudiences(true)
+		Jwt jwt = jwtGenerator.addScopes("openid", "app1.scope", "app2.sub.scope", "app2.scope", ".scopeWithoutAppId").deriveAudiences(true)
 				.getToken();
 
 		assertThat(jwt.getAudience().size(), equalTo(2));
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenImplTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenImplTest.java
index 0d2f596c..7f78edb7 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenImplTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/TokenImplTest.java
@@ -35,10 +35,10 @@ public class TokenImplTest {
 	private Jwt jwtCC;
 	private Jwt jwtCCNoAttributes;
 	private JWTClaimsSet.Builder claimsSetBuilder = null;
-	private String xsAppName = "my-app-name!400";
+	private String xsAppName = "my-app-name!t400";
 	private String scopeRead = xsAppName + "." + "Read";
 	private String scopeWrite = xsAppName + "." + "Write";
-	private String scopeOther = "other-app-name!777.Other";
+	private String scopeOther = "other-app-name!t777.Other";
 	private String userName = "testUser";
 	private String zoneId = "e2f7fbdb-0326-40e6-940f-dfddad057ff3";
 
@@ -74,6 +74,7 @@ public class TokenImplTest {
 		assertThat(token.isAccountNonExpired(), is(true));
 		assertThat(token.getAuthorities().size(), is(0));
 		assertThat(token.isEnabled(), is(false));
+		assertThat(token.getExpirationDate(), is(JwtGenerator.NO_EXPIRE_DATE));
 		assertThat(token.getAdditionalAuthAttribute("any"), nullValue());
 	}
 
diff --git a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java
index 9d70ebfd..09e7384c 100644
--- a/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java
+++ b/spring-xsuaa/src/test/java/com/sap/cloud/security/xsuaa/token/authentication/XsuaaAudienceValidatorTest.java
@@ -1,5 +1,8 @@
 package com.sap.cloud.security.xsuaa.token.authentication;
 
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.is;
+
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
@@ -53,9 +56,18 @@ public class XsuaaAudienceValidatorTest {
 
 	@Test
 	public void testSameClientIdWithoutAudience() {
-		OAuth2TokenValidatorResult result2 = new XsuaaAudienceValidator(serviceConfigurationSameClientId)
+		OAuth2TokenValidatorResult result = new XsuaaAudienceValidator(serviceConfigurationSameClientId)
 				.validate(tokenWithoutAudience);
-		Assert.assertFalse(result2.hasErrors());
+		Assert.assertFalse(result.hasErrors());
+	}
+
+	@Test
+	public void testExtractAudiencesFromTokenScopes() {
+		Jwt token = new JwtGenerator().addScopes("test1!t1.read","test2!t1.read","test2!t1.write", ".scopeWithoutAppId").getToken();
+		List<String> audiences = new XsuaaAudienceValidator(serviceConfigurationSameClientId).getAllowedAudiences(token);
+		Assert.assertThat(audiences.size(), is(2));
+		Assert.assertThat(audiences, hasItem("test1!t1"));
+		Assert.assertThat(audiences, hasItem("test2!t1"));
 	}
 
 	@Test