Repository: SAP/cloud-security-xsuaa-integration
Pull request #288 (2020-05-18 13:06:49)

Remaining buildpack issues
- This PR add getSubdomain to XsuaaToken.
- It deprecates requestToken in favor of requestTokenForClient in XSUserInfo.

Open questions

- Should [getSubdomain](https://github.com/SAP/cloud-security-xsuaa-integration/compare/RemainingBuildpackIssues?expand=1#diff-ca26559b5d64e33866652654b33d6a0aR115) in XSUserInfo also get a `@deprecated` javadoc?

- Should the `@deprecated` annotation stay in `requestTokenForClient`?

I have also removed the public modifiers in the interface because they are redundant.


Patch:
diff --git a/README.md b/README.md
index 0e2696a1..675842ee 100644
--- a/README.md
+++ b/README.md
@@ -64,8 +64,7 @@ Spring Boot provides OAuth resource servers. Application developers requiring au
 See [spring-security-xsuaa-usage](samples/spring-security-xsuaa-usage) for an example.
 
 ### Additional (test) utilities
-- [spring-xsuaa-mock](./spring-xsuaa-mock) offers a Mock web server to provide token keys for offline token validation in case xsuaa service (OAuth resource-server) is not in place. Its use is only intended for unit tests and when running your Spring boot application on a Webserver locally.
-- [spring-xsuaa-test](./spring-xsuaa-test) offers test utilities to generate custom JWT tokens for the purpose of tests.
+- [java-security-test](./java-security-test) offers test utilities to generate custom JWT tokens for the purpose of tests. It pre-configures a [WireMock](http://wiremock.org/docs/getting-started/) web server to stub outgoing calls to the identity service (OAuth resource-server), e.g. to provide token keys for offline token validation. Its use is only intended for JUnit tests.
 
 Find [here](https://github.com/SAP/cloud-application-security-sample/tree/master/spring-security-basis) a more sophisticated sample.
 
diff --git a/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java b/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java
index f3f20edb..9747256f 100644
--- a/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java
+++ b/api/src/main/java/com/sap/xsa/security/container/XSUserInfo.java
@@ -34,7 +34,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getLogonName() throws XSUserInfoException;
+	String getLogonName() throws XSUserInfoException;
 
 	/**
 	 * Given name of the user.
@@ -47,7 +47,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getGivenName() throws XSUserInfoException;
+	String getGivenName() throws XSUserInfoException;
 
 	/**
 	 * Familiy name of the user.
@@ -60,7 +60,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getFamilyName() throws XSUserInfoException;
+	String getFamilyName() throws XSUserInfoException;
 
 	/**
 	 * Return the user origin. The origin is an alias that refers to a user store in
@@ -75,7 +75,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getOrigin() throws XSUserInfoException;
+	String getOrigin() throws XSUserInfoException;
 
 	/**
 	 * Return identity zone which is the same like the subaccount id (tenant id).
@@ -100,17 +100,19 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getSubaccountId() throws XSUserInfoException;
+	String getSubaccountId() throws XSUserInfoException;
 
 	/**
-	 * Still Supported via {@code XSUserInfoAdapter} from the
-	 * {@code com.sap.cloud.security.adapter.xs} package.
+	 *
+	 * Supported via {@code XSUserInfoAdapter} from the
+	 * {@code com.sap.cloud.security.adapter.xs} package. Also available on tokens
+	 * of type {@code XsuaaToken} from java-security.
 	 * 
 	 * @return the subdomain
 	 * @throws XSUserInfoException
 	 *             if subdomain is not available in the authentication token
 	 */
-	public String getSubdomain() throws XSUserInfoException;
+	String getSubdomain() throws XSUserInfoException;
 
 	/**
 	 * Return the client id of the authentication token
@@ -122,7 +124,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getClientId() throws XSUserInfoException;
+	String getClientId() throws XSUserInfoException;
 
 	/**
 	 * @deprecated Can be replaced with {@code token.getClaimAsString(attribute)}
@@ -134,7 +136,7 @@ public interface XSUserInfo {
 	 *             if attribute is not available in the authentication token
 	 */
 	@Deprecated
-	public String getJsonValue(String attribute) throws XSUserInfoException;
+	String getJsonValue(String attribute) throws XSUserInfoException;
 
 	/**
 	 * Return the email of the user
@@ -147,7 +149,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getEmail() throws XSUserInfoException;
+	String getEmail() throws XSUserInfoException;
 
 	/**
 	 * @deprecated use {@link #getHdbToken()} instead.
@@ -156,7 +158,7 @@ public interface XSUserInfo {
 	 *             if db token is not available in the authentication token
 	 */
 	@Deprecated
-	public String getDBToken() throws XSUserInfoException;
+	String getDBToken() throws XSUserInfoException;
 
 	/**
 	 * Still Supported via {@code XSUserInfoAdapter} from the
@@ -166,7 +168,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if db token is not available in the authentication token
 	 */
-	public String getHdbToken() throws XSUserInfoException;
+	String getHdbToken() throws XSUserInfoException;
 
 	/**
 	 * Return authentication token
@@ -175,7 +177,7 @@ public interface XSUserInfo {
 	 *             {@code com.sap.cloud.security.token} package.
 	 * @return authentication token
 	 */
-	public String getAppToken();
+	String getAppToken();
 
 	/**
 	 * Still Supported via {@code XSUserInfoAdapter} from the
@@ -192,7 +194,7 @@ public interface XSUserInfo {
 	 * @deprecated use {@link #getHdbToken()} instead.
 	 */
 	@Deprecated
-	public String getToken(String namespace, String name) throws XSUserInfoException;
+	String getToken(String namespace, String name) throws XSUserInfoException;
 
 	/**
 	 * Return user attributes.
@@ -206,7 +208,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String[] getAttribute(String attributeName) throws XSUserInfoException;
+	String[] getAttribute(String attributeName) throws XSUserInfoException;
 
 	/**
 	 * Check if the authentication token contains user attributes. Still Supported
@@ -217,7 +219,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public boolean hasAttributes() throws XSUserInfoException;
+	boolean hasAttributes() throws XSUserInfoException;
 
 	/**
 	 * Still Supported via {@code XSUserInfoAdapter} from the
@@ -229,7 +231,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if system attribute is not available in the authentication token
 	 */
-	public String[] getSystemAttribute(String attributeName) throws XSUserInfoException;
+	String[] getSystemAttribute(String attributeName) throws XSUserInfoException;
 
 	/**
 	 * Check if a scope is present in the authentication token
@@ -242,7 +244,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public boolean checkScope(String scope) throws XSUserInfoException;
+	boolean checkScope(String scope) throws XSUserInfoException;
 
 	/**
 	 * Check if a "local" scope is available in the authentication token according
@@ -257,7 +259,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public boolean checkLocalScope(String scope) throws XSUserInfoException;
+	boolean checkLocalScope(String scope) throws XSUserInfoException;
 
 	/**
 	 * Return additional authentication attributes included by the OAuth client
@@ -272,7 +274,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getAdditionalAuthAttribute(String attributeName) throws XSUserInfoException;
+	String getAdditionalAuthAttribute(String attributeName) throws XSUserInfoException;
 
 	/**
 	 * In case of xsuaa broker plan tokens, it contains the service instance id
@@ -284,7 +286,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getCloneServiceInstanceId() throws XSUserInfoException;
+	String getCloneServiceInstanceId() throws XSUserInfoException;
 
 	/**
 	 * OAuth Grant Type used for this token
@@ -298,7 +300,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public String getGrantType() throws XSUserInfoException;
+	String getGrantType() throws XSUserInfoException;
 
 	/**
 	 * Check if a token issued for another OAuth client has been forwarded to a
@@ -309,7 +311,7 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if attribute is not available in the authentication token
 	 */
-	public boolean isInForeignMode() throws XSUserInfoException;
+	boolean isInForeignMode() throws XSUserInfoException;
 
 	/**
 	 * @param clientId
@@ -324,19 +326,22 @@ public interface XSUserInfo {
 	 * @throws XSUserInfoException
 	 *             if an error occurs during token request
 	 */
-	@Deprecated
-	public String requestTokenForClient(String clientId, String clientSecret, String uaaUrl) throws XSUserInfoException;
+	String requestTokenForClient(String clientId, String clientSecret, String uaaUrl) throws XSUserInfoException;
 
 	/**
 	 * Exchange a token into a token from another service instance
-	 * 
+	 *
 	 * @param tokenRequest
 	 *            request data
 	 * @deprecated can be replaced with token flows from the token-client library.
+	 *             For client credentials flow you can also use
+	 *             {@link #requestTokenForClient(String, String, String)}. Will be
+	 *             removed with version 3.0.0.
 	 * @return requested token
 	 * @throws XSUserInfoException
 	 *             if an error occurs during token exchange
 	 */
-	public String requestToken(XSTokenRequest tokenRequest) throws XSUserInfoException;
+	@Deprecated
+	String requestToken(XSTokenRequest tokenRequest) throws XSUserInfoException;
 
 }
diff --git a/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java b/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java
index 6c6356a5..e69a10be 100644
--- a/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java
+++ b/java-api/src/main/java/com/sap/cloud/security/token/TokenClaims.java
@@ -31,8 +31,9 @@ public final class TokenClaims {
 		public static final String ZONE_ID = "zid"; // tenant GUID, identity zone id, subaccount id
 		public static final String CLIENT_ID = "cid"; // OAuth client identifier
 		public static final String SCOPES = "scope"; // list of scopes including app id, e.g. "my-app!t123.Display"
-		public static final String EXTERNAL_ATTRIBUTE = "ext_attr";
 		public static final String ISSUED_AT = "iat";
+		public static final String EXTERNAL_ATTRIBUTE = "ext_attr";
+		public static final String EXTERNAL_ATTRIBUTE_ZDN = "zdn";
 		public static final String EXTERNAL_ATTRIBUTE_ENHANCER = "enhancer";
 
 		public static final String TRUSTED_CLIENT_ID_SUFFIX = "trustedclientidsuffix";
diff --git a/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java b/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java
index 1b017f83..b89c7386 100644
--- a/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java
+++ b/java-security/src/main/java/com/sap/cloud/security/token/XsuaaToken.java
@@ -10,6 +10,7 @@ import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.security.Principal;
 import java.util.LinkedHashSet;
+import java.util.Optional;
 import java.util.Set;
 
 import static com.sap.cloud.security.token.TokenClaims.USER_NAME;
@@ -145,4 +146,23 @@ public class XsuaaToken extends AbstractToken implements AccessToken {
 		return GrantType.from(getClaimAsString(GRANT_TYPE));
 	}
 
+	/**
+	 * Returns the value of the subdomain (zdn) from the external attribute ext_attr
+	 * (ext_attr) claim. If the external attribute or the subdomain is missing, it
+	 * returns {@code null}.
+	 *
+	 * @return the subdomain or {@code null}
+	 */
+	@Nullable
+	public String getSubdomain() {
+		return getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, EXTERNAL_ATTRIBUTE_ZDN);
+	}
+
+	@Nullable
+	private String getAttributeFromClaimAsString(String claimName, String attributeName) {
+		return Optional.ofNullable(getClaimAsJsonObject(claimName))
+				.map(claim -> claim.getAsString(attributeName))
+				.orElse(null);
+	}
+
 }


Test patch:
diff --git a/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java b/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java
index 0d95ff71..9155bfa7 100644
--- a/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java
+++ b/java-security/src/test/java/com/sap/cloud/security/token/XsuaaTokenTest.java
@@ -105,10 +105,13 @@ public class XsuaaTokenTest {
 
 	@Test
 	public void getAudiences() {
-		assertThat(clientCredentialsToken.getAudiences()).isNotEmpty();
-		assertThat(clientCredentialsToken.getAudiences()).hasSize(2);
-		assertThat(clientCredentialsToken.getAudiences()).contains("uaa");
-		assertThat(clientCredentialsToken.getAudiences()).contains("sap_osb");
+		assertThat(clientCredentialsToken.getAudiences()).containsExactlyInAnyOrder("uaa", "sap_osb");
+	}
+
+	@Test
+	public void getSubdomain() {
+		assertThat(clientCredentialsToken.getSubdomain()).isNull();
+		assertThat(userToken.getSubdomain()).isEqualTo("theSubdomain");
 	}
 
 }
\ No newline at end of file
diff --git a/java-security/src/test/resources/xsuaaUserAccessTokenRSA256.txt b/java-security/src/test/resources/xsuaaUserAccessTokenRSA256.txt
index fb179553..cf4c641d 100644
--- a/java-security/src/test/resources/xsuaaUserAccessTokenRSA256.txt
+++ b/java-security/src/test/resources/xsuaaUserAccessTokenRSA256.txt
@@ -1,1 +1,1 @@
-eyJqa3UiOiJodHRwOi8vYXV0aC5jb20vdG9rZW5fa2V5cyIsImtpZCI6ImtleS1pZC0wIiwiYWxnIjoiUlMyNTYifQ==.eyJhdWQiOlsiY2xpZW50SWQiXSwiZ3JhbnRfdHlwZSI6InVzZXJfdG9rZW4iLCJ1c2VyX25hbWUiOiJ0ZXN0VXNlciIsIm9yaWdpbiI6InVzZXJJZHAiLCJpc3MiOiJodHRwOi8vYXV0aC5jb20iLCJleHAiOiI2OTc0MDMxNjAwIiwiY2lkIjoiY2xpZW50SWQifQ==.Da8DJ3n46aVLEkJvk0a5Gj9g2h2w9kCVfaKEpnUr5PXJNtkY7VdnMRaZe9DoOuUIH3LUJjbw4NVh6j4V8tUe_jjqJwmYCn3DnlHuFL8jeCKyg6f3SmwQ0rkFopKANdSh3Uaq5l7NXHWAl-A3wQ0kFjEKC6DydF5EfcQzEH6K1EQcYZncEhRO4fUh4KxwUmF2ydyIeb5R7yyQBL7VchZgMi6oQrFhsR8za576Sz-lZTUB03CBRqnc24dqnM5VjTh-LPSXGCQVkT70u-3Nm9lXj4PQeGBz41C-Wiy3bKljBUefCO1f5jhOPdxU63A_w878_MKEEueH3PcMoNfsPAnpcQ==
\ No newline at end of file
+eyJqa3UiOiJodHRwOi8vYXV0aC5jb20vdG9rZW5fa2V5cyIsImtpZCI6ImtleS1pZC0wIiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOlsiY2xpZW50SWQiXSwiZXh0X2F0dHIiOnsiZW5oYW5jZXIiOiJYU1VBQSIsInpkbiI6InRoZVN1YmRvbWFpbiJ9LCJncmFudF90eXBlIjoidXNlcl90b2tlbiIsInVzZXJfbmFtZSI6InRlc3RVc2VyIiwib3JpZ2luIjoidXNlcklkcCIsImlzcyI6Imh0dHA6Ly9hdXRoLmNvbSIsImV4cCI6IjY5NzQwMzE2MDAiLCJjaWQiOiJjbGllbnRJZCJ9.eSagbm6JtHhy8C1YiF9seN8-oTilXHtacDQt0AVNFKAy9xpO10GJNfm9t6b7MgvON9rnwkvzGloThMRVVxZw2TjaPpZ5vzMIEei3gl4x_oVzrc8Cu2hDvxrH-w2u0keHCkoNratadXDSD3YrsObnouwWIYjqbZMo8zq_0rBmykyEq1kqQIKx6lmd4OZkUS8zjZ_0qaDEgYr--b4RYmkJh9Ee20jXqX2wxAhPrzSlMwsbj8V752WGAGTyx6LPReFbZNkEKSp0zDhDNQW2Yl2DtUuJFvF1Pn5CgEbPMPVqujO7sdjZSwy4wqPPqMUibS_XBdp1wK7Patnrm5DvGQFS-Q
\ No newline at end of file