All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sap.cloud.security.adapter.xs.XSUserInfoAdapter Maven / Gradle / Ivy

package com.sap.cloud.security.adapter.xs;

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.json.JsonObject;
import com.sap.cloud.security.json.JsonParsingException;
import com.sap.cloud.security.token.AccessToken;
import com.sap.cloud.security.token.GrantType;
import com.sap.cloud.security.xsuaa.Assertions;
import com.sap.cloud.security.xsuaa.client.*;
import com.sap.cloud.security.xsuaa.tokenflows.TokenFlowException;
import com.sap.cloud.security.xsuaa.tokenflows.XsuaaTokenFlows;
import com.sap.xsa.security.container.XSTokenRequest;
import com.sap.xsa.security.container.XSUserInfo;
import com.sap.xsa.security.container.XSUserInfoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;

import static com.sap.cloud.security.token.TokenClaims.*;
import static com.sap.cloud.security.token.TokenClaims.XSUAA.*;

/**
 * This class implements the {@link XSUserInfo} interface by wrapping and
 * delegating calls to an {@link AccessToken}.
 *
 * Other implementations of {@link XSUserInfo} support loading the token from
 * the the spring security context holder. This is not supported by this class!
 * It also does not support the SAPAuthorizationExtension.
 *
 */
public class XSUserInfoAdapter implements XSUserInfo {

	static final String EXTERNAL_CONTEXT = "ext_ctx";
	static final String CLAIM_ADDITIONAL_AZ_ATTR = "az_attr";
	static final String XS_USER_ATTRIBUTES = "xs.user.attributes";
	// new with SECAUTH-806
	static final String XS_SYSTEM_ATTRIBUTES = "xs.system.attributes";
	static final String CLAIM_SUBACCOUNT_ID = "subaccountid";
	static final String HDB_NAMEDUSER_SAML = "hdb.nameduser.saml";
	static final String SERVICEINSTANCEID = "serviceinstanceid";
	static final String ZDN = "zdn";
	static final String SYSTEM = "SYSTEM";
	static final String HDB = "HDB";

	private static final Logger LOGGER = LoggerFactory.getLogger(XSUserInfoAdapter.class);

	private final AccessToken accessToken;
	private final OAuth2ServiceConfiguration configuration;
	/**
	 * Use {@link #getOrCreateOAuth2TokenService()} for access.
	 */
	private OAuth2TokenService oAuth2TokenService;

	public XSUserInfoAdapter(Object accessToken) {
		this(accessToken, Environments.getCurrent().getXsuaaConfiguration());
	}

	public XSUserInfoAdapter(AccessToken accessToken) {
		this(accessToken, Environments.getCurrent().getXsuaaConfiguration());
	}

	XSUserInfoAdapter(Object accessToken, OAuth2ServiceConfiguration configuration) {
		if (!(accessToken instanceof AccessToken)) {
			String type = Objects.isNull(accessToken) ? null : accessToken.getClass().getName();
			throw new XSUserInfoException("token is of instance " + type
					+ " but needs to be an instance of AccessToken.");
		}
		this.accessToken = (AccessToken) accessToken;
		this.configuration = configuration;
	}

	/**
	 * Loading the token from the security context like this is not supported!
	 *
	 * XSUserInfoAdapter() { Authentication auth =
	 * SecurityContextHolder.getContext().getAuthentication(); if (auth instanceof
	 * OAuth2Authentication) { SAPAuthorizationExtension extension =
	 * (SAPAuthorizationExtension) ((OAuth2Authentication)
	 * auth).getOAuth2Request().getExtensions().get("sap"); if (extension != null) {
	 * this.foreignMode = extension.isForeignMode(); } } }
	 */
	@Override
	public String getLogonName() {
		checkNotGrantTypeClientCredentials("getLogonName");
		return getClaimValue(USER_NAME);
	}

	@Override
	public String getGivenName() {
		checkNotGrantTypeClientCredentials("getGivenName");
		String externalAttributeName = getExternalAttribute(GIVEN_NAME);
		if (externalAttributeName == null) {
			return getClaimValue(GIVEN_NAME);
		} else {
			return externalAttributeName;
		}
	}

	@Override
	public String getFamilyName() {
		checkNotGrantTypeClientCredentials("getFamilyName");
		String externalAttributeName = getExternalAttribute(FAMILY_NAME);
		if (externalAttributeName == null) {
			return getClaimValue(FAMILY_NAME);
		} else {
			return externalAttributeName;
		}
	}

	@Override
	public String getOrigin() {
		checkNotGrantTypeClientCredentials("getOrigin");
		return getClaimValue(ORIGIN);
	}

	@Override
	public String getIdentityZone() {
		return getClaimValue(ZONE_ID);
	}

	@Override
	public String getSubaccountId() {
		String subaccountId = getAttributeFromClaimAsString(XS_SYSTEM_ATTRIBUTES, CLAIM_SUBACCOUNT_ID);
		return (subaccountId == null || "".equals(subaccountId)) ? getClaimValue(ZONE_ID) : subaccountId;
	}

	@Override
	public String getZoneId() {
		return accessToken.hasClaim(SAP_GLOBAL_ZONE_ID) ? accessToken.getClaimAsString(SAP_GLOBAL_ZONE_ID) : getClaimValue(ZONE_ID);
	}

	@Override
	/**
	 * "ext_attr": { "enhancer": "XSUAA", "zdn": "paas-subdomain" },
	 */
	public String getSubdomain() {
		return Optional.ofNullable(getExternalAttribute(ZDN)).orElse(null);
	}

	@Override
	public String getClientId() {
		return getClaimValue(CLIENT_ID);
	}

	@Override
	public String getJsonValue(String attribute) {
		return getClaimValue(attribute);
	}

	@Override
	public String getEmail() {
		checkNotGrantTypeClientCredentials("getEmail");
		return getClaimValue(EMAIL);
	}

	@Override
	public String getDBToken() {
		return getHdbToken();
	}

	@Override
	public String getHdbToken() {
		return getToken(SYSTEM, HDB);
	}

	@Override
	public String getAppToken() {
		return accessToken.getTokenValue();
	}

	@Override
	public String getToken(String namespace, String name) {
		if (!(getGrantType().equals(GrantType.CLIENT_CREDENTIALS.toString())) && hasAttributes() && isInForeignMode()) {
			throw new XSUserInfoException("The SecurityContext has been initialized with an access token of a\n"
					+ "foreign OAuth Client Id and/or Identity Zone. Furthermore, the\n"
					+ "access token contains attributes. Due to the fact that we want to\n"
					+ "restrict attribute access to the application that provided the \n"
					+ "attributes, the getToken function does not return a valid token");
		}
		if (!namespace.equals(SYSTEM)) {
			throw new XSUserInfoException("Invalid namespace " + namespace);
		}
		if (name.equals(HDB)) {
			String token;
			if (accessToken.hasClaim(EXTERNAL_CONTEXT)) {
				token = getAttributeFromClaimAsString(EXTERNAL_CONTEXT, HDB_NAMEDUSER_SAML);
			} else {
				token = accessToken.getClaimAsString(HDB_NAMEDUSER_SAML);
			}
			if (token == null) {
				token = accessToken.getTokenValue();
			}
			return token;
		} else if (name.equals("JobScheduler")) {
			return accessToken.getTokenValue();
		} else {
			throw new XSUserInfoException("Invalid name " + name + " for namespace " + namespace);
		}
	}

	@Override
	public String[] getAttribute(String attributeName) {
		checkNotGrantTypeClientCredentials("getAttribute");
		return getMultiValueAttributeFromExtObject(XS_USER_ATTRIBUTES, attributeName);
	}

	@Override
	public boolean hasAttributes() {
		checkNotGrantTypeClientCredentials("hasAttributes");
		if (accessToken.hasClaim(EXTERNAL_CONTEXT)) {
			JsonObject extContext = getClaimAsJsonObject(EXTERNAL_CONTEXT);
			return extContext != null && extContext.contains(XS_USER_ATTRIBUTES) && !extContext
					.getJsonObject(EXTERNAL_CONTEXT).isEmpty();
		} else {
			JsonObject xsUserAttributes = getClaimAsJsonObject(XS_USER_ATTRIBUTES);
			return !(xsUserAttributes == null || xsUserAttributes.isEmpty());
		}
	}

	@Override
	public String[] getSystemAttribute(String attributeName) {
		return getMultiValueAttributeFromExtObject(XS_SYSTEM_ATTRIBUTES, attributeName);
	}

	@Override
	public boolean checkScope(String scope) {
		return accessToken.hasScope(scope);
	}

	@Override
	public boolean checkLocalScope(String scope) {
		try {
			return accessToken.hasLocalScope(scope);
		} catch (IllegalArgumentException e) {
			throw new XSUserInfoException(e.getMessage());
		}
	}

	@Override
	public String getAdditionalAuthAttribute(String attributeName) {
		return Optional.ofNullable(getAttributeFromClaimAsString(CLAIM_ADDITIONAL_AZ_ATTR, attributeName))
				.orElseThrow(createXSUserInfoException(attributeName));
	}

	@Override
	public String getCloneServiceInstanceId() {
		return Optional.ofNullable(getExternalAttribute(SERVICEINSTANCEID))
				.orElseThrow(createXSUserInfoException(SERVICEINSTANCEID));
	}

	@Override
	public String getGrantType() {
		return Optional.ofNullable(accessToken.getGrantType())
				.map(GrantType::toString)
				.orElseThrow(createXSUserInfoException(GRANT_TYPE));
	}

	/**
	 * Check if a token issued for another OAuth client has been forwarded to a
	 * different client,
	 *
	 * This method does not support checking if the token can be accepted by ACL.
	 *
	 * @return true if token was forwarded or if it cannot be determined.
	 */
	@Override
	public boolean isInForeignMode() {
		if (configuration == null) {
			LOGGER.info("No configuration provided -> falling back to foreignMode = true!");
			return true; // default provide OAuth2ServiceConfiguration via constructor argument
		}
		String tokenClientId, tokenIdentityZone;
		try {
			tokenClientId = getClientId();
			tokenIdentityZone = getIdentityZone();
		} catch (XSUserInfoException e) {
			LOGGER.warn("Tried to access missing attribute when checking for foreign mode", e);
			return true;
		}
		boolean clientIdsMatch = tokenClientId.equals(configuration.getClientId());
		boolean identityZonesMatch = tokenIdentityZone
				.equals(configuration.getProperty(CFConstants.XSUAA.IDENTITY_ZONE));
		boolean isApplicationPlan = tokenClientId.contains("!t");
		boolean isBrokerPlan = tokenClientId.contains("!b");

		if (clientIdsMatch && (identityZonesMatch || isApplicationPlan || isBrokerPlan)) {
			LOGGER.info(
					"Token not in foreign mode because because client ids  match and identityZonesMatch={}, isApplicationPlan={} ",
					identityZonesMatch, isApplicationPlan);
			return false; // no foreign mode
		}
		// in case of broker master: check trustedclientidsuffix
		String bindingTrustedClientIdSuffix = configuration.getProperty(TRUSTED_CLIENT_ID_SUFFIX);
		if (bindingTrustedClientIdSuffix != null && tokenClientId.endsWith(bindingTrustedClientIdSuffix)) {
			LOGGER.info("Token not in foreign mode because token client id matches binding trusted client suffix");
			return false; // no foreign mode
		}
		LOGGER.info(
				"Token in foreign mode: clientIdsMatch={}, identityZonesMatch={}, isApplicationPlan={}, bindingTrustedClientIdSuffix={}",
				clientIdsMatch, identityZonesMatch, isApplicationPlan, bindingTrustedClientIdSuffix);
		return true;
	}

	@Override
	public String requestTokenForClient(String clientId, String clientSecret, String baseUaaUrl) {
		return performTokenFlow(baseUaaUrl, XSTokenRequest.TYPE_CLIENT_CREDENTIALS_TOKEN, clientId, clientSecret,
				new HashMap<>());
	}

	@Override
	public String requestTokenForUser(String clientId, String clientSecret, String baseUaaUrl) {
		return performTokenFlow(baseUaaUrl, XSTokenRequest.TYPE_USER_TOKEN, clientId, clientSecret, new HashMap<>());
	}

	@Override
	public String requestToken(XSTokenRequest tokenRequest) {
		Assertions.assertNotNull(tokenRequest, "TokenRequest argument is required");
		if (!tokenRequest.isValid()) {
			throw new XSUserInfoException("Invalid grant type or missing parameters for requested grant type.");
		}
		String tokenEndpoint = tokenRequest.getTokenEndpoint().toString();
		String baseUaaUrl = tokenEndpoint.replace(tokenRequest.getTokenEndpoint().getPath(), "");
		Map additionalAuthAttributes = tokenRequest.getAdditionalAuthorizationAttributes();
		return performTokenFlow(baseUaaUrl, tokenRequest.getType(), tokenRequest.getClientId(),
				tokenRequest.getClientSecret(), additionalAuthAttributes);
	}

	/**
	 * Tries to create an OAuth2TokenService and throws
	 * UnsupportedOperationException if it fails.
	 *
	 * @throws UnsupportedOperationException
	 *             if it cannot create the service.
	 * @return the created OAuth2TokenService
	 */
	private OAuth2TokenService getOrCreateOAuth2TokenService() {
		if (oAuth2TokenService == null) {
			oAuth2TokenService = tryToCreateDefaultOAuth2TokenService();
			if (oAuth2TokenService == null) {
				oAuth2TokenService = tryToCreateXsuaaOAuth2TokenService();
			}
		}
		if (oAuth2TokenService == null) {
			throw new UnsupportedOperationException("Failed to create OAuth2TokenService. "
					+ "Make sure your project has a dependency to either spring-web or apache HTTP client.");
		}
		return oAuth2TokenService;
	}

	/**
	 * This method tries to create a {@link DefaultOAuth2TokenService} instance
	 * which can fail because the required dependency (apache HTTP client) might be
	 * missing. In this case a {@link java.lang.NoClassDefFoundError} is thrown
	 * which is a {@link LinkageError} that needs to be caught in addition to
	 * exceptions!
	 *
	 * @return the {@link DefaultOAuth2TokenService} instance or null if it could
	 *         not be created.
	 */
	private OAuth2TokenService tryToCreateDefaultOAuth2TokenService() {
		LOGGER.debug("Trying to create DefaultOAuth2TokenService.");
		try {
			return new DefaultOAuth2TokenService();
		} catch (Exception | LinkageError e) {
			LOGGER.debug("Failed to create DefaultOAuth2TokenService.", e);
		}
		return null;
	}

	/**
	 *
	 * Similar to {@link #tryToCreateDefaultOAuth2TokenService()} except it tries to
	 * create {@link XsuaaOAuth2TokenService} and internally depends on spring-web.
	 *
	 * @return the {@link XsuaaOAuth2TokenService} or null if it could not be
	 *         created.
	 */
	private OAuth2TokenService tryToCreateXsuaaOAuth2TokenService() {
		LOGGER.debug("Trying to create XsuaaOAuth2TokenService.");
		try {
			return new XsuaaOAuth2TokenService();
		} catch (Exception | LinkageError e) {
			LOGGER.debug("Failed to create XsuaaOAuth2TokenService.", e);
		}
		return null;
	}

	// for tests
	void setOAuth2TokenService(OAuth2TokenService oAuth2TokenService) {
		this.oAuth2TokenService = oAuth2TokenService;
	}

	private String[] getMultiValueAttributeFromExtObject(String claimName, String attributeName) {
		JsonObject claimAsJsonObject = getClaimAsJsonObject(claimName);
		return Optional.ofNullable(claimAsJsonObject)
				.map(jsonObject -> jsonObject.getAsList(attributeName, String.class))
				.map(values -> values.toArray(new String[] {}))
				.orElseThrow(createXSUserInfoException(attributeName));
	}

	private void checkNotGrantTypeClientCredentials(String methodName) {
		if (GrantType.CLIENT_CREDENTIALS == accessToken.getGrantType()) {
			String message = String.format("Method '%s' is not supported for grant type '%s'", methodName,
					GrantType.CLIENT_CREDENTIALS);
			throw new XSUserInfoException(message + GrantType.CLIENT_CREDENTIALS);
		}
	}

	@Nullable
	private String getAttributeFromClaimAsString(String claimName, String attributeName) {
		return Optional.ofNullable(getClaimAsJsonObject(claimName))
				.map(claim -> claim.getAsString(attributeName)).orElse(null);
	}

	private Supplier createXSUserInfoException(String attribute) {
		return () -> new XSUserInfoException("Invalid user attribute " + attribute);
	}

	private String getClaimValue(String claimname) {
		String value = accessToken.getClaimAsString(claimname);
		if (value == null) {
			throw new XSUserInfoException("Invalid user attribute " + claimname);
		}
		return value;
	}

	@Nullable
	private JsonObject getClaimAsJsonObject(String claimName) {
		try {
			return accessToken.getClaimAsJsonObject(claimName);
		} catch (JsonParsingException e) {
			throw createXSUserInfoException(claimName).get();
		}
	}

	String getExternalAttribute(String attributeName) {
		return getAttributeFromClaimAsString(EXTERNAL_ATTRIBUTE, attributeName);
	}

	/**
	 * Getter for XsuaaTokenFlows object that can be overridden for testing
	 * purposes.
	 */
	XsuaaTokenFlows getXsuaaTokenFlows(String baseUaaUrl, ClientCredentials clientCredentials) {
		return new XsuaaTokenFlows(getOrCreateOAuth2TokenService(),
				new XsuaaDefaultEndpoints(baseUaaUrl), clientCredentials);
	}

	private String performTokenFlow(String baseUaaUrl, int tokenRequestType, String clientId, String clientSecret,
			Map additionalAuthAttributes) {
		try {
			ClientCredentials clientCredentials = new ClientCredentials(clientId, clientSecret);
			XsuaaTokenFlows xsuaaTokenFlows = getXsuaaTokenFlows(baseUaaUrl, clientCredentials);
			return performRequest(xsuaaTokenFlows, tokenRequestType, additionalAuthAttributes);
		} catch (RuntimeException e) {
			throw new XSUserInfoException(e.getMessage());
		}
	}

	private String performRequest(XsuaaTokenFlows xsuaaTokenFlows, int tokenRequestType,
			Map additionalAuthAttributes) {
		switch (tokenRequestType) {
		case XSTokenRequest.TYPE_USER_TOKEN:
			return performUserTokenFlow(xsuaaTokenFlows, additionalAuthAttributes);
		case XSTokenRequest.TYPE_CLIENT_CREDENTIALS_TOKEN:
			return performClientCredentialsFlow(xsuaaTokenFlows, additionalAuthAttributes);
		default:
			throw new XSUserInfoException(
					"Found unsupported XSTokenRequest type. The only supported types are XSTokenRequest.TYPE_USER_TOKEN and XSTokenRequest.TYPE_CLIENT_CREDENTIALS_TOKEN.");
		}
	}

	private String performUserTokenFlow(XsuaaTokenFlows xsuaaTokenFlows, Map additionalAuthAttributes) {
		String userToken;
		try {
			userToken = xsuaaTokenFlows.userTokenFlow()
					.subdomain(getSubdomain())
					.token(getAppToken())
					.attributes(additionalAuthAttributes)
					.execute().getAccessToken();
		} catch (TokenFlowException e) {
			LOGGER.error("Error performing User Token Flow", e);
			throw new XSUserInfoException("Error performing User Token Flow.", e);
		}
		return userToken;
	}

	private String performClientCredentialsFlow(XsuaaTokenFlows xsuaaTokenFlows,
			Map additionalAuthAttributes) {
		String ccfToken;
		try {
			ccfToken = xsuaaTokenFlows.clientCredentialsTokenFlow()
					.subdomain(getSubdomain())
					.attributes(additionalAuthAttributes)
					.execute().getAccessToken();
		} catch (TokenFlowException e) {
			LOGGER.error("Error performing Client Credentials Flow", e);
			throw new XSUserInfoException("Error performing Client Credentials Flow..", e);
		}
		return ccfToken;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy