WSO2 Identity Server is a very strong CIAM tool that is used across industries and geographical locations. To serve and support end-users across geographical location about technical issues in any tech platform ‘language’ is a very big hurdle. But if the customer-service representative can see the ‘users’ perspective’ in the platform, it could help to avoid the language barrier, understand issue faster and thereby leading to a faster resolution time.
“User impersonation” is a mechanism through which customer support representatives can help end-users by impersonating them. This feature in not available by default in WSO2 Identity Server. With this requirement, I have developed and implemented a “custom grant” type to bridge the gap.
Some background information:
The stack consists of WSO2 APIM, IS-KM and a web-application. The web-application is secured by IS-KM and during login an access token is generated for end-user. The web-application uses API published in APIM to fetch end-user details and to access other services. A JWT token is generated in the GW which is passed on the BE, end-user details in the JWT heavily determines the response of the API. As the API is user sensitive the best way to impersonate the end-user is generating an access token on behalf his behalf.
Once this access token is generated then customer-service representative can access the web application the same way the end-user does. The GW will generate a JWT Token (with end-user details) which can be used to deliver personalized services by the web application.
High Level diagram:
How it works:
As described earlier I have built a custom grant type and named it user_impersonation. This custom grant type user_impersonation which accepts access token of the customer-service representative and generates a new access token for a target user. The flow works on the following order:
- Customer-service representative will invoke the user_impersonation grant type.
- Grant type validates if all the required params i.e. grant_type, target_user and access_token are provided.
- Check if the customer-service representative access token is valid.
- Check if the customer-service representative has proper role thereby its authorization.
- Generate the access token for the end-user (target_user).
- Use the generate access token to invoke the API.
- API-GW will validate the end-user (target_user) access token with Identity Server.
- API-GW will generate the users JWT token for end-user (target_user) and pass to BE services.
- This JWT will used in the BE services to serve end-user (target_user) specific data like order status, product list, shopping cart etc. Now the customer-service representative has impersonated the end-user (target_user).
Sample postman call to APIM looks like:
Configuration in Identity Server for custom grant type:
- Add the below config at {IS-Home}/repository/conf/identity.xml
- Build the jar by `mvn clean install` and deploy .jar file in `{IS-Home}/repository/components/dropins/`
- Sample code for the user_impersonation grant type is shown below.
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;
}
}
Conclusion:
With this approach we can successfully impersonate an end-user in WSO2 Identity server.
Yenlo is the leading, global, multi-technology integration specialist in the field of API-management, Integration technology and Identity Management. Known for our strong focus on best-of-breed hybrid and cloud-based iPaaS technologies. Yenlo is the product leader and multi-award winner in WSO2, Boomi, Microsoft Azure and AWS technologies and offers best-of-breed solutions from multiple leading integration vendors.
With over 200 experts in the API, integration, and Identity Access Management domain and over $35 million in annual revenue, Yenlo is one of the largest and best API-first and Cloud-first integration specialists worldwide.