WSO2 Identity Server ist ein sehr leistungsfähiges CIAM-Tool, das branchen- und standortübergreifend eingesetzt wird. Die Betreuung und Unterstützung von Endnutzern an verschiedenen geografischen Standorten ist eine große Hürde, wenn es um technische Fragen in einer beliebigen Technologieplattform-„Sprache“ geht. Aber wenn der Kundendienst-mitarbeiter die „Benutzerperspektive“ auf der Plattform sehen kann, könnte dies helfen, die Sprachbarriere zu umgehen, Probleme schneller zu verstehen und dadurch zu einer schnelleren Lösung zu kommen.
„Identitätswechsel“ ist ein Mechanismus, durch den Kundendienstmitarbeiter Endbenutzern helfen können, indem sie sich als diese ausgeben. Diese Funktion ist in WSO2 Identity Server standardmäßig nicht verfügbar. Mit dieser Anforderung habe ich einen „benutzerdefinierten Genehmigungs “-Typ entwickelt und implementiert, um die Lücke zu schließen.
Einige Hintergrundinformationen:
Der Stack besteht aus WSO2 APIM, IS-KM und einer Webanwendung. Die Webanwendung wird durch IS-KM gesichert und bei der Anmeldung wird ein Zugriffstoken für den Endbenutzer generiert. Die Webanwendung verwendet die in APIM veröffentlichte API, um Endbenutzerdaten abzurufen und auf andere Dienste zuzugreifen. Im GW wird ein JWT-Token generiert, der an das BE weitergegeben wird. Die Endbenutzerdaten im JWT bestimmen maßgeblich die Antwort der API. Da die API benutzerspezifisch ist, kann man sich am besten als Endbenutzer ausgeben, wenn man in seinem Namen ein Zugriffstoken erzeugt.
Sobald dieses Zugriffstoken generiert ist, kann der Kundendienstmitarbeiter auf die gleiche Weise auf die Webanwendung zugreifen wie der Endbenutzer. Das GW generiert ein JWT-Token (mit Endbenutzerdaten), das von der Webanwendung zur Bereitstellung personalisierter Dienste verwendet werden kann.
High Level diagramm:
Wie es funktioniert:
Wie zuvor beschrieben, habe ich einen benutzerdefinierten Genehmigungstyp erstellt und ihn user_impersonation genannt. Dieser benutzerdefinierte Genehmigungstyp user_impersonation akzeptiert das Zugriffstoken des Kundendienstmitarbeiters und erzeugt ein neues Zugriffstoken für einen Zielbenutzer. Der Ablauf funktioniert wie folgt:
- Der Kundendienstmitarbeiter ruft den Genehmigungstyp user_impersonation auf.
- Der Genehmigungstyp überprüft, ob alle erforderlichen Parameter, d. h. grant_type, target_user und access_token, vorhanden sind.
- Überprüfen Sie, ob das Zugangstoken des Kundendienstmitarbeiters gültig ist.
- Überprüfen Sie, ob der Endbenutzer (target_user) im Benutzerspeicher vorhanden ist.
- Controleer of de vertegenwoordiger van de klantenservice de juiste rol heeft bij zijn autorisatie.
- Generieren Sie das Zugriffstoken für den Endbenutzer (target_user).
- Verwenden Sie das generierte Zugriffstoken, um die API aufzurufen.
- Das API-GW überprüft das Zugriffstoken des Endbenutzers (target_user) mit Identity Server.
- Das API-GW generiert das JWT-Token des Benutzers für den Endbenutzer (target_user) und leitet es an die BE-Dienste weiter.
- Dieses JWT wird in den BE-Diensten verwendet, um endbenutzerspezifische Daten (target_user) wie Bestellstatus, Produktliste, Warenkorb usw. bereitzustellen. Jetzt hat sich der Kundendienstmitarbeiter als Endbenutzer (target_user) ausgegeben.
Ein Beispiel für einen Postman-Call an APIM sieht so aus:
Konfiguration in Identity Server für benutzerdefinierten Genehmigungstyp:
- Fügen Sie die folgende Konfiguration zu {IS-Home}/repository/conf/identity.xml hinzu.
- Erstellen Sie die Jar-Datei mit „mvn clean install“ und setzen Sie die .jar-Datei in „{IS-Home}/repository/components/dropins/“ ein.
- Im Folgenden finden Sie einen Beispielcode für den Genehmigungstyp user_impersonation.
package org.wso2.custom.grant;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wso2.carbon.base.MultitenantConstants;
import org.wso2.carbon.identity.application.common.model.ApplicationPermission;
import org.wso2.carbon.identity.base.IdentityRuntimeException;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationRequestDTO;
import org.wso2.carbon.identity.oauth2.model.AccessTokenDO;
import org.wso2.carbon.identity.oauth2.model.RequestParameter;
import org.wso2.carbon.identity.oauth2.token.OAuthTokenReqMessageContext;
import org.wso2.carbon.identity.oauth2.token.handlers.grant.AbstractAuthorizationGrantHandler;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;
import org.wso2.carbon.identity.oauth2.validators.TokenValidationHandler;
import org.wso2.carbon.user.api.UserStoreException;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.carbon.utils.multitenancy.MultitenantUtils;
import java.util.ArrayList;
import java.util.Arrays;
public class MimickingGrantHandler extends AbstractAuthorizationGrantHandler {
private static Log log = LogFactory.getLog(MimickingGrantHandler.class);
public static final Logger audit_log = LoggerFactory.getLogger("AUDIT_LOG");
@Override
public boolean validateGrant(OAuthTokenReqMessageContext tokReqMsgCtx) throws IdentityOAuth2Exception {
if(!super.validateGrant(tokReqMsgCtx)){
return false;
}
boolean targetUserExist = false;
boolean isBackUserAuthorized = false;
RequestParameter [] oauth2RequestParameter=tokReqMsgCtx.getOauth2AccessTokenReqDTO().getRequestParameters();
String accessToken="";
String targetUser="";
ArrayList<String> scopeList= new ArrayList<>();
for (RequestParameter requestParam:oauth2RequestParameter) {
String key = requestParam.getKey().trim();
String value = requestParam.getValue()[0].trim();
if(key.equalsIgnoreCase(MimickingGrantConstants.ACCESS_TOKEN)){
accessToken=value;
}
if(key.equalsIgnoreCase(MimickingGrantConstants.TARGET_USER)){
targetUser=value;
}
if(key.equalsIgnoreCase(MimickingGrantConstants.SCOPE)){
scopeList.addAll(Arrays.asList(value.split(MimickingGrantConstants.SPACE)));
}
}
boolean isValidToken=validateAccessToken(accessToken,MimickingGrantConstants.BEARER);
AccessTokenDO accessTokenDO ;
if(isValidToken){
accessTokenDO= OAuth2Util.getAccessTokenDOfromTokenIdentifier(accessToken);
}else {
throw new IdentityOAuth2Exception("access token is invalid" );
}
if(log.isDebugEnabled()) {
log.debug(" Access Token status : " +isValidToken +"\n"+
"Back office user : "+accessTokenDO.getAuthzUser().getUserName()+"\n"+
"Target_User = "+ targetUser +"\n");
}
/*
* check if the target user exist and
* if user have valid token
*/
targetUserExist=isTargetUserExist(targetUser);
isBackUserAuthorized=isUserAuthorized(accessToken,getSPPermittedRoleList());
if (isBackUserAuthorized && targetUserExist) {
tokReqMsgCtx.setAuthorizedUser(OAuth2Util.getUserFromUserName(targetUser));
scopeList.add(MimickingGrantConstants.USER_MIMICKING);
String [] scope=scopeList.toArray(new String[scopeList.size()]);
tokReqMsgCtx.setScope(scope);
} else {
if(!targetUserExist){
throw new IdentityOAuth2Exception( targetUser +" doesn't exist" );
}
else {
throw new IdentityOAuth2Exception("Authorization failed for " + accessTokenDO.getAuthzUser().getUserName());
}
}
audit_log.info("Mimicking_Back-Office_User = "+accessTokenDO.getAuthzUser().getUserName() +" Target_User = "+ targetUser +" Status = Completed User_Authenticated = "+ (isBackUserAuthorized && targetUserExist));
return isBackUserAuthorized && targetUserExist;
}
private boolean validateAccessToken(String accessToken,String accessTokenType){
try {
TokenValidationHandler validationHandler = TokenValidationHandler.getInstance();
OAuth2TokenValidationRequestDTO validationRequestDTO = new OAuth2TokenValidationRequestDTO();
OAuth2TokenValidationRequestDTO.OAuth2AccessToken oAuth2AccessToken= validationRequestDTO.new OAuth2AccessToken();
oAuth2AccessToken.setIdentifier(accessToken);
oAuth2AccessToken.setTokenType(accessTokenType);
validationRequestDTO.setAccessToken(oAuth2AccessToken);
return validationHandler.validate(validationRequestDTO).isValid();
} catch (IdentityOAuth2Exception e) {
e.printStackTrace();
log.error("error while validating access token");
}
return false;
}
/**
*
* Check if the impersonator user have the
* @param accessToken
* @return
*/
private boolean isUserAuthorized(String accessToken,ArrayList <String> permittedRoleList){
try {
AccessTokenDO accessTokenDO= OAuth2Util.getAccessTokenDOfromTokenIdentifier(accessToken);
OAuth2Util.getServiceProvider(accessTokenDO.getConsumerKey()).getSpProperties();
String impersonatorUserName=accessTokenDO.getAuthzUser().getUserName();
String tenantAwareUserName = MultitenantUtils.getTenantAwareUsername(impersonatorUserName);
String userTenantDomain = MultitenantUtils.getTenantDomain(impersonatorUserName);
String impersonatorCompleteUserName = tenantAwareUserName + "@" + userTenantDomain;
int tenantId = IdentityTenantUtil.getTenantIdOfUser(impersonatorCompleteUserName);
RealmService realmService = IdentityTenantUtil.getRealmService();
UserStoreManager userStoreManager = realmService.getTenantUserRealm(tenantId).getUserStoreManager();
String [] roles = userStoreManager.getRoleListOfUser(
MimickingGrantConstants.USER_STORE_DOMAIN+
MimickingGrantConstants.FORWARD_SLASH+
impersonatorUserName);
ArrayList<String> rolesList = new ArrayList<String>(Arrays.asList(roles));
rolesList.retainAll(permittedRoleList);
if(rolesList.size()>=1){
return true;
}
} catch (UserStoreException | IdentityOAuth2Exception e) {
log.error("error occurred while authorizing user "+e.getMessage());
}
return false;
}
private boolean isTargetUserExist(String targetUser) {
String tenantAwareUserName = MultitenantUtils.getTenantAwareUsername(targetUser);
String userTenantDomain = MultitenantUtils.getTenantDomain(targetUser);
String targetUserCompleteUserName = tenantAwareUserName + "@" + userTenantDomain;
int tenantId = MultitenantConstants.INVALID_TENANT_ID;
try {
tenantId = IdentityTenantUtil.getTenantIdOfUser(targetUserCompleteUserName);
} catch (IdentityRuntimeException e) {
log.error("Token request with Mimicking Grant Type for an invalid tenant : " +
MultitenantUtils.getTenantDomain(targetUserCompleteUserName));
return false;
}
RealmService realmService = IdentityTenantUtil.getRealmService();
try {
UserStoreManager userStoreManager = realmService.getTenantUserRealm(tenantId).getUserStoreManager();
return userStoreManager.isExistingUser(targetUser);
} catch (UserStoreException e) {
log.error("error occurred while searching the user "+ targetUser +" ; "+e.getMessage());
}
return false;
}
/**
* Get permitted role list which is allowed to use this grant type
* @return
*/
private ArrayList getSPPermittedRoleList(){
ArrayList <String> permittedRoleList =new ArrayList <String>();
permittedRoleList.add(MimickingGrantConstants.PERMITTED_ROLES);
return permittedRoleList;
}
}
Conclusie:
Met deze aanpak kunnen we met succes ons voordoen als een eindgebruiker in de WSO2 Identity Server