fb
WSO2 Identity Server 6 min

Impersonating User in Identity Server

sidharth
Sidharth Dash
Integration Specialist
identity
Scroll

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:

highlevel diagram identity server

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:

  1. 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).

  2. 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.

  3. 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:

werking

Configuration in Identity Server for custom grant type:

  • Add the below config at {IS-Home}/repository/conf/identity.xml 
example identity server code
  • 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.

Full API lifecycle Management Selection Guide

WHITEPAPER

smartmockups l0qqucke