com.sap.cds.feature.identity.IdentityUserInfoProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cds-feature-identity Show documentation
Show all versions of cds-feature-identity Show documentation
API implementation to handle oauth2 tokens (IAS or XSUAA)
The newest version!
/**************************************************************************
* (C) 2019-2024 SAP SE or an SAP affiliate company. All rights reserved. *
**************************************************************************/
package com.sap.cds.feature.identity;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.sap.cds.services.ErrorStatuses;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.authentication.AuthenticationInfo;
import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo;
import com.sap.cds.services.request.UserInfo;
import com.sap.cds.services.runtime.CdsRuntime;
import com.sap.cds.services.runtime.UserInfoProvider;
import com.sap.cds.services.utils.ClassMethods;
import com.sap.cds.services.utils.ErrorStatusException;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
import com.sap.cloud.security.json.JsonParsingException;
import com.sap.cloud.security.token.GrantType;
import com.sap.cloud.security.token.SecurityContext;
import com.sap.cloud.security.token.Token;
import com.sap.cloud.security.token.TokenClaims;
import com.sap.cloud.security.token.XsuaaToken;
public class IdentityUserInfoProvider implements UserInfoProvider {
private final static Logger logger = LoggerFactory.getLogger(IdentityUserInfoProvider.class);
private final CdsRuntime runtime;
private final ServiceBinding iasBinding;
private final ServiceBinding xsuaaBinding;
public IdentityUserInfoProvider(CdsRuntime runtime, Optional iasBinding, Optional xsuaaBinding) {
this.runtime = runtime;
this.xsuaaBinding = xsuaaBinding.orElse(null);
this.iasBinding = iasBinding.orElse(null);
}
private static final String SYSTEM_USER_NAME = "system";
private static final String SYSTEM_INTERNAL_USER_NAME = "system-internal";
@Override
public UserInfo get() {
AuthenticationInfo authenticationInfo = runtime.getProvidedAuthenticationInfo();
if(authenticationInfo != null && authenticationInfo.is(JwtTokenAuthenticationInfo.class)) {
try {
JwtTokenAuthenticationInfo accessToken = authenticationInfo.as(JwtTokenAuthenticationInfo.class);
Token token = Token.create(accessToken.getToken());
logger.debug("Creating UserInfo based on token {}", token);
switch(token.getService()) {
case IAS:
UserInfo iasUser = new IasUserInfoImpl(token);
logger.debug("Resolved {}", iasUser);
return iasUser;
case XSUAA:
UserInfo xsuaaUser = new XsuaaUserInfoImpl((XsuaaToken)token);
logger.debug("Resolved {}", xsuaaUser);
return xsuaaUser;
default:
throw new ErrorStatusException(ErrorStatuses.UNAUTHORIZED); // unsupported auth type
}
} catch (ServiceException e) {
throw e;
} catch (Exception e) { // NOSONAR
throw new ErrorStatusException(ErrorStatuses.UNAUTHORIZED, e); // if there is a authentication the user must be extracted
}
} else {
return null;
}
}
/**
* Similar implementation of {@link UserInfo} as in cds-feature-xsuaa.
* Adjustments in this class potentially need to be double maintained as long as cds-features-xsuaa
* has not been removed.
*/
private class XsuaaUserInfoImpl implements UserInfo {
private final XsuaaToken token;
private final String name;
private final boolean isSystemUser;
private final boolean isInternalUser;
private final Set roles;
private final Map> attributes;
private final Map additionalAttributes;
private static final String SPECIAL_ATTRIBUTE_LOGON_NAME = "logonName";
private static final String SPECIAL_ATTRIBUTE_TENANT = "tenant";
private static final String EXTENSION_ATTRIBUTES = "ext_attr";
private static final String BINDING_CLIENT_ID = "clientid";
private static final String SERVICEINSTANCEID_ATTRIBUTE = "serviceinstanceid";
private static final String SPECIAL_ATTRIBUTE_SERVICEINSTANCEID = EXTENSION_ATTRIBUTES + "." + SERVICEINSTANCEID_ATTRIBUTE;
private final Set KNOWN_CLAIMS = new HashSet<>(Arrays.asList(TokenClaims.USER_NAME,
TokenClaims.XSUAA.ZONE_ID,
"user_id",
TokenClaims.XSUAA.SCOPES,
TokenClaims.XSUAA.XS_USER_ATTRIBUTES,
"xs.system.attributes",
TokenClaims.XSUAA.GRANT_TYPE,
"client_id"
));
@SuppressWarnings("deprecation")
private final Set SYSTEM_USER_GRANTS = new HashSet<>(Arrays.asList(
GrantType.CLIENT_CREDENTIALS, GrantType.CLIENT_X509));
private XsuaaUserInfoImpl(XsuaaToken token) {
this.token = token;
this.isSystemUser = SYSTEM_USER_GRANTS.contains(token.getGrantType());
// an internal user is a technical user from the same client
this.isInternalUser = this.isSystemUser && token.getClientId() != null && token.getClientId().equals( xsuaaBinding.getCredentials().get(BINDING_CLIENT_ID) );
if (this.isInternalUser) {
// on CF system users have no name
this.name = SYSTEM_INTERNAL_USER_NAME;
} else if (this.isSystemUser) {
this.name = SYSTEM_USER_NAME;
} else {
this.name = token.getClaimAsString(TokenClaims.USER_NAME);
}
// filter the scopes. "$XSAPPNAME." - prefix is always directly in front of the scope (if prefixed).
// There might be additional prefixes on top, e.g. "$SERVICEINSTANCEID."
String scopePrefix = (String) xsuaaBinding.getCredentials().get("xsappname") + ".";
this.roles = token.getScopes().stream().map(scope -> {
int pos = scope.indexOf(scopePrefix);
if (pos >= 0) {
return scope.substring(pos + scopePrefix.length());
}
return scope;
}).collect(Collectors.toSet());
// add some specific user attributes ($user.tenant and $user.ext_attr.serviceinstanceid)
this.attributes = new TreeMap<>();
@SuppressWarnings("unchecked")
Map> userAttributes = ((Map>)token.getClaims().get(TokenClaims.XSUAA.XS_USER_ATTRIBUTES));
if (userAttributes != null) {
this.attributes.putAll(userAttributes);
}
this.attributes.put(SPECIAL_ATTRIBUTE_TENANT, Collections.singletonList(token.getZoneId()));
String serviceInstanceId = token.getAttributeFromClaimAsString(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE, SERVICEINSTANCEID_ATTRIBUTE);
if (serviceInstanceId != null) {
this.attributes.put(SPECIAL_ATTRIBUTE_SERVICEINSTANCEID, Collections.singletonList(serviceInstanceId));
}
this.additionalAttributes = new HashMap<>();
this.additionalAttributes.put("givenName", token.getClaimAsString(TokenClaims.GIVEN_NAME));
this.additionalAttributes.put("familyName", token.getClaimAsString(TokenClaims.FAMILY_NAME));
this.additionalAttributes.put("subDomain", token.getAttributeFromClaimAsString(TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE, TokenClaims.XSUAA.EXTERNAL_ATTRIBUTE_ZDN));
token.getClaims().entrySet().stream().filter(entry -> !KNOWN_CLAIMS.contains(entry.getKey())).forEach(entry ->
this.additionalAttributes.put(entry.getKey(), entry.getValue())
);
// add logon name to additional attributes
if (!this.isSystemUser) {
this.additionalAttributes.put(SPECIAL_ATTRIBUTE_LOGON_NAME, token.getPrincipal().getName());
}
}
@Override
public String getId() {
return token.getClaimAsString("user_id");
}
@Override
public String getName() {
return name;
}
@Override
public String getTenant() {
return token.getZoneId();
}
@Override
public Set getRoles() {
return roles; // NOSONAR
}
@Override
public boolean isSystemUser() {
return isSystemUser;
}
@Override
public boolean isInternalUser() {
return isInternalUser; // implies isSystemUser
}
@Override
public boolean isAuthenticated() {
// as there is an accepted token we can be sure that the user is authenticated
return true;
}
@Override
public boolean isPrivileged() {
return false; // XSUAA user is never a privileged user!
}
@Override
public Map> getAttributes() {
return attributes; // NOSONAR
}
@Override
public Map getAdditionalAttributes() {
return additionalAttributes;
}
@Override
public T as(Class userInfoClazz) {
return ClassMethods.as(userInfoClazz, UserInfo.class, this, this::getAdditionalAttributes);
}
@Override
public String toString() {
return MessageFormat.format("XsuaaUserInfo [id=''{0}'', name=''{1}'', roles=''{2}'', attributes=''{3}''",
getId(), getName(), getRoles(), getAttributes());
}
}
private class IasUserInfoImpl implements UserInfo {
private final Token token;
private final Map> attributes;
private final boolean isSystemUser;
private final boolean isInternalUser;
private Set roles = Collections.emptySet();
private static final String BINDING_CLIENT_ID = "clientid";
private static final String SPECIAL_ATTRIBUTE_TENANT = "tenant";
private static final String CLAIM_IAS_APIS = "ias_apis";
// keep in sync with TokenClaims
private static final Set KNOWN_CLAIMS = new HashSet<>(Arrays.asList(
TokenClaims.IAS_ISSUER,
TokenClaims.ISSUER,
TokenClaims.EXPIRATION,
TokenClaims.AUDIENCE,
TokenClaims.NOT_BEFORE,
TokenClaims.SUBJECT,
TokenClaims.SAP_GLOBAL_USER_ID,
"zone_uuid", // TokenClaims.SAP_GLOBAL_ZONE_ID
"app_tid", // TokenClaims.SAP_GLOBAL_APP_TID
TokenClaims.AUTHORIZATION_PARTY,
TokenClaims.CNF,
TokenClaims.CNF_X5T,
TokenClaims.XSUAA.ISSUED_AT,
"jti"));
IasUserInfoImpl(Token token) {
this.token = token;
// map all unknown claims to attribute lists
this.attributes = token.getClaims().entrySet().stream().filter(e -> !KNOWN_CLAIMS.contains(e.getKey()))
.collect(Collectors.toMap(e -> e.getKey(), e -> {
try {
return token.getClaimAsStringList(e.getKey());
} catch(JsonParsingException ex) {
return Collections.singletonList(token.getClaimAsString(e.getKey()));
}}));
this.attributes.put(SPECIAL_ATTRIBUTE_TENANT, Collections.singletonList(getTenant()));
// 'azp' will only be provided as dedicated claim if 'aud' is multi-value.
// in some scenarios (e.g. App2App), 'azp' and 'aud' might be available as single-value claim. 'azp' needs to take precedence.
List aud = token.getClaimAsStringList(TokenClaims.AUDIENCE);
String azp = token.getClaimAsString(TokenClaims.AUTHORIZATION_PARTY);
if ((azp == null) && (aud.size() == 1)) {
azp = aud.get(0);
}
this.isSystemUser = azp.equals(token.getClaimAsString(TokenClaims.SUBJECT));
this.isInternalUser = isSystemUser && azp.equals(iasBinding.getCredentials().get(BINDING_CLIENT_ID));
if (runtime.getEnvironment().getCdsProperties().getSecurity().getIdentity().isExposePlansAsRoles()) {
// granted access to 'provided-apis' from App2App and 'service plans' from App2Service are mapped to roles
this.roles = new HashSet<>();
if (token.hasClaim(CLAIM_IAS_APIS)) {
this.roles.addAll(token.getClaimAsStringList(CLAIM_IAS_APIS).stream().map(String::trim).collect(Collectors.toSet()));
}
List servicePlans = SecurityContext.getServicePlans();
if (servicePlans != null) {
this.roles.addAll(servicePlans.stream().map(String::trim).collect(Collectors.toSet()));
}
}
}
@Override
public boolean isSystemUser() {
return isSystemUser;
}
@Override
public boolean isInternalUser() {
return isInternalUser;
}
@Override
public String getId() {
return token.getClaimAsString(TokenClaims.SAP_GLOBAL_USER_ID);
}
@Override
@SuppressWarnings("deprecation")
public String getTenant() {
return token.getZoneId();
}
@Override
public Set getRoles() { return roles; }
@Override
public String getName() {
if(isInternalUser) {
return SYSTEM_INTERNAL_USER_NAME;
}
else if (isSystemUser) {
return SYSTEM_USER_NAME;
} else {
// "sub" is best match for the logon name.
// IAS migration tools will make sure it matches the email by default.
return token.getClaimAsString(TokenClaims.SUBJECT);
}
}
@Override
public boolean isAuthenticated() {
// as there is an accepted token we can be sure that the user is authenticated
return true;
}
@Override
public boolean isPrivileged() {
return false; // IAS user is never a privileged user!
}
@Override
public Map> getAttributes() {
return attributes;
}
@Override
public Map getAdditionalAttributes() {
return token.getClaims();
}
@Override
public String toString() {
return MessageFormat.format("IasUserInfo [id=''{0}'', name=''{1}'', roles=''{2}'', attributes=''{3}''",
getId(), getName(), getRoles(), getAttributes());
}
}
}