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

com.sap.cds.feature.xsuaa.XsUaaToken Maven / Gradle / Ivy

The newest version!
/**************************************************************************
 * (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
 **************************************************************************/
package com.sap.cds.feature.xsuaa;

import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cds.services.utils.CdsErrorStatuses;
import com.sap.cds.services.utils.ErrorStatusException;

/**
 * A parser that converts Base64-encoded bearer tokens issued by XSUAA.
 */
public class XsUaaToken {

	private static final String BEARER_TOKEN = "bearer";

	private static final String CLAIM_GIVEN_NAME = "given_name";

	private static final String CLAIM_FAMILY_NAME = "family_name";

	private static final String CLAIM_ZDN = "zdn";

	// mandatory properties:

	private String id;

	private String tenant;

	private String grantType;

	// optional properties:

	@JsonProperty("user_name")
	private String name;

	@JsonProperty("scope")
	private List scopes = new ArrayList<>();

	@JsonProperty("client_id")
	private String clientId;

	@JsonProperty("xs.user.attributes")
	private Map> userAttributes = new HashMap<>();

	@JsonProperty("xs.system.attributes")
	private Map> systemAttributes = new HashMap<>();

	@JsonProperty("ext_attr")
	private Map extendedAttributes = new HashMap<>();

	@JsonAnySetter
	// all other not explicitly mapped attributes are collected in this map:
	private Map additionalAttributes = new HashMap<>();

	/**
	 * Grant types of special interest
	 */
	public static enum GrantType {
		CLIENT_CREDENTIALS("client_credentials"),
		CLIENT_X509("client_x509");

		public final String id;

		GrantType(String id) {
			this.id = id;
		}

		@Override
		public String toString() {
			return this.id;
		}
	}

	/**
	 * The shared JSON object mapper
	 */
	private static ObjectMapper jsonMapper = createObjectMapper();

	/**
	 * Constructor for JSON parser. user_id, zid and grant_typ must be present for
	 * construction.
	 *
	 * @param id	The user id
	 * @param zid	The tenant
	 * @param grantType	The grant type of this token
	 */
	private XsUaaToken(
			@JsonProperty(value = "user_id", required = false) String id,
			@JsonProperty(value = "zid", required = true) String zid,
			@JsonProperty(value = "grant_type", required = true) String grantType
			) {
		this.id = id;
		this.tenant = zid;
		this.grantType = grantType;
	}

	/**
	 * Tries to extract the token representation from the raw authorization header value.
	 *
	 * @param authorizationHeader	The authorization header value
	 * @return	The token representation
	 *
	 * @throws IllegalArgumentException	in case the token could not be parsed.
	 */
	public static XsUaaToken parse(String authorizationHeader) {
		XsUaaToken decodedJwtToken = decodeJwtToken(authorizationHeader);
		decodedJwtToken.initComputedAttributes();
		return decodedJwtToken;
	}

	private void initComputedAttributes() {
		// provide whole ext_attr structure
		additionalAttributes.put("ext_attr", extendedAttributes);

		// some extension attributes overrule the direct token attributes
		String givenName = tryCastString(getExtensionAttributes().get(CLAIM_GIVEN_NAME));
		if(givenName == null) {
			givenName = tryCastString(additionalAttributes.get(CLAIM_GIVEN_NAME));
		}
		additionalAttributes.put("givenName", givenName); // needs to match XsuaaUserInfo::getGivenName()

		String familyName = tryCastString(getExtensionAttributes().get(CLAIM_FAMILY_NAME));
		if(familyName == null) {
			familyName = tryCastString(additionalAttributes.get(CLAIM_FAMILY_NAME));
		}
		additionalAttributes.put("familyName", familyName); // needs to match XsuaaUserInfo::getFamilyName()

		String subDomain = tryCastString(getExtensionAttributes().get(CLAIM_ZDN));
		additionalAttributes.put("subDomain", subDomain); // needs to match XsuaaUserInfo::getSubDomain()

		// TODO: ensure method names
		// XsuaaUserInfo.class.getMethod(givenName, parameterTypes)
	}

	private String tryCastString(Object value) {
		return value instanceof String s ? s : null;
	}

	private static XsUaaToken decodeJwtToken(String authorizationHeader) {

		String jwtRaw = authorizationHeader.trim();
		if (jwtRaw.substring(0, Math.min(BEARER_TOKEN.length(), authorizationHeader.length() - 1)).toLowerCase(Locale.ENGLISH).equals(BEARER_TOKEN)) {
			jwtRaw = jwtRaw.substring(BEARER_TOKEN.length());
			jwtRaw = jwtRaw.trim();
		}

		String[] jwtParts = jwtRaw.split("\\.");
		if(jwtParts.length == 3) {
			String base64EncodedBody = jwtParts[1];
			String jwtInfo = new String(Base64.getUrlDecoder().decode(base64EncodedBody)); // ISO-8859-1 //NOSONAR

			try {
				return jsonMapper.readValue(jwtInfo, XsUaaToken.class);
			} catch(Exception e) { // NOSONAR
				throw new ErrorStatusException(CdsErrorStatuses.TOKEN_PARSING_FAILED, e);
			}
		}

		throw new ErrorStatusException(CdsErrorStatuses.TOKEN_PARSING_FAILED);
	}


	public String getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public String getTenant() {
		return tenant;
	}

	public String getGrantType() {
		return grantType;
	}

	public String getClientId() {
		return clientId;
	}

	public List getScopes() {
		return scopes; // NOSONAR
	}

	public Map> getUserAttributes() {
		return userAttributes;
	}

	public Map> getSystemAttributes() {
		return systemAttributes;
	}

	public Map getExtensionAttributes() {
		return extendedAttributes;
	}

	public Map getAdditionalAttributes() {
		return additionalAttributes;
	}

	@Override
	public String toString() {
		try {
			return jsonMapper.writeValueAsString(this);
		} catch (JsonProcessingException e) {
			return "";
		}
	}

	private static ObjectMapper createObjectMapper() {
		ObjectMapper mapper = new ObjectMapper();

		// there are much more properties which we don't interpret (and which could change any time)
		mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

		// attributes value lists often have only a single item which then are not encoded as arrays but as single value
		mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);

		// this makes the serialization (toString()) omitting null properties
		mapper.setSerializationInclusion(Include.NON_NULL);

		return mapper;
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy