When Salesforce is life!

Tag: ChatBot

[Salesforce] How to safely get the logged in community user from Einstein Bot

 
Few weeks ago I blogged about Dealing with the running User on Einstein Bot dialogs.

One of the questions I got from the community was:

is this approach safe for production?

I was actually not sure about this, as the logged in info was passed from the pre-chat page to the chat bot context in clear: this way an advanced JS hacker could have impersonated a real user, knowing its username/user id: that’s why I replied that you should have used a temporary “token”, that should have been deleted once used (upon user confirmation on the Chat Bot loading step).

When I write posts about tech workaround all I have in mind is open your minds and make you find a new way to solve problem: from this point we can enhance our solution to make it the best one!

Few days ago Arthur Imirzian contacted me to read his new article on his blog: How to safely get the logged in community user from Einstein Bot.

This blog was the direct consequence of my workaround, making the user identification safer.
He then went on by finding another workaround to enforce user authentication with this great post!

I loved his quick and dirty style and I decided, upon his approval, to repost his article here.

Enjoy the reading!


Kerckhoffs’s principle says that a cryptosystem should be secure even if everything about the system, except the key, is public knowledge. The user id is not a key and shouldn’t be considered as one.

Both classic Live Agent client and Snap-Ins chat client provide a very useful feature to pass the prechat information to the custom fields on LiveChatTranscript object.

As you know, Einstein bot is built on top of Live Agent and allows you to trigger apex actions based on community user message like a knowledge article search. Problems come when you want to make authenticated actions like updating your case status or contact information. Live Agent doesn’t provide any way to enforce logged community user session. Instead, the context user is a technical user (AutomatedProcess or Integration).

The first reflex is to get the user id from a visualforce page / lightning component and pass it to the Snap-Ins Code Snippet in order to reuse it from apex by querying the Transcript Object. This approach is a security breach as the user id can be easily changed on the fly. The attacker can usurp any user by providing another user id. Again, user id is not a private key and shouldn’t be used as one.

A safer approach

So we need to find a way to validate that the returned user id is equal to the connected user id. The idea illustrated below suggest asymmetrically hashing the user id so no one can change it to usurp someone else’s identity. As the end user cannot produce a token, it can’t corrupt the user id.

Step 1: Token generation

The first step is about token generation based on connected user id. The user id will be publicly accessible but we will use a self-signed certificate to sign the user id and generate a token.

Go to Setup > Security > Certificate and Key Management > Create Self-Signed Certificate, define a label, a unique name, uncheck Exportable Private Key and click on save.

Use the apex code below to generate a secure token based on session id and user id of the connected user.

public static String getToken(String keyToSign){
    if(keyToSign == null){
        return null;
    }
    return EncodingUtil.base64Encode(
        Crypto.signWithCertificate(
            'RSA-SHA256',
            Blob.valueOf(keyToSign),
            'LightningComponent'
        )
    );
}

Step 2: Get the user id and the token

Add the lightning:prechatUI interface to a Lightning component to enable Snap-ins to identify it as a custom Lightning page template that can be used for the Snap-ins Chat pre-chat page (docs here).

Now call the getPrechatData method from your lightning component to retrieve the logged user id and its token.

@AuraEnabled
public static Map<String,String> getPrechatData() {
    Map<String,String> prechatData = new Map<String,String>();
    // Returns the context user's ID
    String userId = UserInfo.getUserId();
    prechatData.put('userId',userId);
    String token = getToken(userId);
    prechatData.put('token',token);
    return prechatData;
}

Step 3: Pass the prechat information

LiveChatTranscript is the unique object that can be used to share context between the logged user and Eintein Bot which means that we need to create custom fields on LiveChatTranscript object.

Go to Setup > Object Manager and search for the Live Chat Transcript object. Then Field & Relationships to create two new custom fields called UserId (UserId__c) and Token (Token__c).

Now we have retrieved the user id and the token from step 2, we can pass them through a snippet setting file.

Add the code below to your existing Snippet Settings File you had uploaded as a static resource.

embedded_svc.snippetSettingsFile.extraPrechatInfo = [
    {
        "label":"UserId",
        "transcriptFields":[ "UserId__c" ],
        "displayToAgent": false
    },
    {
        "label":"Token",
        "transcriptFields":[ "Token__c" ],
        "displayToAgent": false
    }
];

Don’t hesitate to visit Nerd At Work’s blog for more details.

Step 4: Validate the token and trust the user id

This is the last step before we can rely on the returned user id. We need to ensure that nothing has been corrupted. We’re going to sign the user id returned by the lightning component again and compare this new token to the returned token.

public static Boolean validateToken(String userId, String token){
    return token == getToken(userId);
}

If the user id was corrupted by the end user, signing it again will produce a different token than the previous one. If not, it means we can now rely on it.

Conclusion

We just described how to safely get the connected community user id from a live agent session. And remember you should never trust user input, your user will not always submit data your application will expect.

What’s the next step ? First pass the Einstein Bots Basics module and then jump to the second article about authenticated DML operation from Einstein BOT.

[Salesforce] Dealing with the running User on Einstein Bot dialogs

 
As part of the Salesforce Solutions Team at WebResults I spend some time training myself on new products and trying to build POCs (prove of concepts) to give value to the training and promote the knowledge inside my company.

I recently was playing with Einstein Bots and got stuck when needed to identify the running user, when dealing with queries to get actual user info from the CRM.

I though it was enough to call the UserInfo.getUserId() method and I could get who is the user calling the bot, but unfortunately the running user of an Apex InvocableMethod is the AutomatedProcess user, that you can monitor from Setup > Debug Logs > User Trace Flags [New] as shown below:

I believe that Salesforce will add this feature in the next feature but I had to find a workaround.

The Workaround

I started a thread on Stackoverflow, where I got some ideas but no solution at all.

After a while I came up with a dirty solution that allowed me to know the exact user logged into the community. I won’t explain all the trial and errors but the whole solution, with some code samples.

Here is the list of actions:

  1. Enable Pre-Chat
  2. Override Pre-Chat with a Lightning Component
  3. The Pre-chat Lightning component calls its controller to get if there is a valid running user and returns back all the data needed for the prechat
  4. The Lightning component compiles all the needed inputs for the pre-chat (e.g. name, email and username) and submits for the chat: if the info are found, the component hides the pre-chat fields (that may be filled by a non authenticated user)
  5. The info submitted are written on the Transcript object
  6. An Einstein Bot dialog examines the Transcript object using an IncobaleMethod of an Apex class and determines which is the running user
  7. The bot can now change its behavior depending on the fact that the user is logged or not

To get all of this working you need 2 custom fields:

  • A custom field on the Contact/Lead record (we’ll call it Username__c) that is used to configure the prechat on the Snap-In
  • A custom field on the LiveChatTranscript object (we’ll call it Username__c as well) used to store the info got from the Pre-Chat component

Once fields are created, we need to setup the Pre-Chat page on the Snap-in settings related to the bot:

And setup the Pre-chat page:

Now let’s create the Lightning component that will replace the Snap-in chat (configured in the picture above):

prechat.cmp

For the what & whys plese refer to the official documentation in this link.

The component uses the lightningsnapin:prechatAPI that is needed to trigger the chat once the info are filled in.

<aura:component
    implements="lightningsnapin:prechatUI"
    controller="Bot_PreChatCmpCnt">
     
    <aura:attribute name="userId" access="PRIVATE" type="string" default="-"/>
    <aura:attribute name="firstName" access="PRIVATE" type="string" />
    <aura:attribute name="lastName" access="PRIVATE" type="string" />
    <aura:attribute name="email" access="PRIVATE" type="string" />
 
    <!-- Contains methods for getting pre-chat fields, starting a chat, and validating fields -->
    <lightningsnapin:prechatAPI aura:id="prechatAPI"/>
     
 
    <!-- After this component has rendered, call the controller's onRender function -->
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
     
    <aura:renderIf isTrue="{!empty(v.userId)}">
        <lightning:input type="text" value="{!v.firstName}" label="Name *">
        <lightning:input type="text" value="{!v.lastName}" label="Lastname *">
        <lightning:input type="text"  value="{!v.email}" label="Email *">
         
        <lightning:button label="Start chat!" onclick="{!c.onStartButtonClick}"/>
    </aura:renderIf>
</aura:component>

The list of input fields are only shown when the v.userId attribute is blank (you can note that it is initialized with “-“, and in the lightning controller is blanked only when no logged user is found).

prechatController.js

The controller calls the Apex controller to get the main user info: if they are found, the chat is started.

({
doInit: function(component, event, helper) {
        var action = component.get("c.getCurrentUser");
        action.setCallback(this, function(response) {
            var state = response.getState();
            if (state === "SUCCESS") {
                var result = JSON.parse(response.getReturnValue());
                console.log(result, embedded_svc);
                component.set('v.userId', result.userId);
                if(result.userId){
                    component.set('v.firstName', result.firstName);
                    component.set('v.lastName', result.lastName);
                    component.set('v.email', result.email);
                    helper.startChat(component, event, helper);
                }
            }else if (state === "ERROR") {
                var errors = response.getError();
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        console.log("Error message: " +
                                 errors[0].message);
                    }
                } else {
                    console.log("Unknown error");
                }
            }
        });
        $A.enqueueAction(action);
    },
    onStartButtonClick: function(component, event, helper) {
        //handling errors
        if(!component.get('v.firstName')
          || !component.get('v.lastName')
           || !component.get('v.email')) return alert('Missing fields.');
        helper.startChat(component, event, helper);
    }
});

prechatHelper.js

The startChat() method compiles the array of fields to be passed to the chat plugin, that will start the chat with the Bot.

({
     
    startChat: function(component, event, helper){
        var fields = [
            {
                label: 'FirstName',
                name: 'FirstName',
                value: component.get('v.firstName')
            } ,
            {
                label: 'LastName',
                name: 'LastName',
                value: component.get('v.lastName')
            }  ,
            {
                label: 'Email',
                name: 'Email',
                value: component.get('v.email')
            },{
                label: 'Username',
                name: 'Username__c',
                value: component.get('v.userId'),
            }
        ];
        if(component.find("prechatAPI").validateFields(fields).valid) {
            component.find("prechatAPI").startChat(fields);
        }
    }
});

prechatHelper.js

The Apex controller simply makes a query on the user object and considers it a loggedin user if the ContactId field is not null (but you can use your own conditions):

public class Bot_PreChatCmpCnt {
    @auraenabled
    public static String getCurrentUser(){
        Map<String,Object> output = new Map<String,Object>();
        User u = [Select Username, FirstName, LastName, Email, contactId From User Where Id = :UserInfo.getUserId()];
        if(u.ContactId != null){
            output.put('userId', u.UserName);
            output.put('firstName', u.FirstName);
            output.put('lastName', u.LastName);
            output.put('email', u.Email);
        }else{
            output.put('userId', '');
        }
        return JSON.serialize(output);
    }
}

That said we need to instruct the Pre-Chat plugin to write the fields on the LiveChatTranscript object, which is the only thing that grants the Bot’s dialogs to be aware of the running context. Create a new Static Resource and remember to set the Cache Control to Public.

embedded_svc.snippetSettingsFile.extraPrechatFormDetails = [
  { 
    "label":"Username", "transcriptFields":[ "Username__c" ],
  },{
    "label":"Cognome", "transcriptFields": ["LastName__c"]
  },{
   "label":"Nome", "transcriptFields": ["FirstName__c"]
  },{
   "label":"Email", "transcriptFields": ["Email__c"]
}];

This Static Resource must be configured in the Snap-in Component on the Community builder:

Einstein Bot Dialog Apex action

Last step, before configuring the bot, is to create a new Apex Class that will retrieve the users information coming from the current transcript:

public without sharing class Bot_GetSnapInsPreChatData {
    public class PrechatOutput{
        @InvocableVariable
        public String sFirstName;
        @InvocableVariable
        public String sLastName;
        @InvocableVariable
        public String sEmail;
        @InvocableVariable
        public String sContactID;
        @InvocableVariable
        public String sLoggedUser;
    }
    public class PrechatInput{
        @InvocableVariable
        public String sChatKey;
    }
    @InvocableMethod(label='Get SnapIns Prechat Data')
    public static List<PrechatOutput> getSnapInsPrechatData(List<PrechatInput> inputParameters)
    {
        System.debug('######## Input Parameters: '+inputParameters);
         
        String sChatKey = inputParameters[0].sChatKey;
        String sContactId = null;
        List outputParameters = new List();
        PrechatOutput outputParameter = new PrechatOutput();
        if (sChatKey != null && sChatKey != '')
        {
            List<LiveChatTranscript> transcripts = [SELECT Id, CaseId,
                                                    ContactId, Username__c
                                                    FROM LiveChatTranscript WHERE ChatKey = :sChatKey];
            if (transcripts.size()>0)
            {
                sContactId = transcripts[0].ContactId;
                outputParameter.sLoggedUser = transcripts[0].Username__c;
            }
        }
         
        if (sContactId != null && sContactId != '')
        {
            List<Contact> contacts = [SELECT Id, FirstName, LastName, Email, Username__c
                                      FROM Contact WHERE Id = :sContactId];
            if (contacts.size()>0)
            {
                outputParameter.sFirstName = contacts[0].FirstName;
                outputParameter.sLastName = contacts[0].LastName;
                outputParameter.sEmail = contacts[0].Email;
                outputParameter.sContactId = contacts[0].Id;
            }
        }
        outputParameters.add(outputParameter);
        return outputParameters;
    }
}

This class read the coming sChatKey value to query the LiveChatTranscript object that stores all the informations coming from the PreChat.

This LiveChatTranscript can be further manipulated to add more and more data for your context.

You must also define a hidden Bot variable containing the Live chat transcript key, that must be called LiveAgentSessionId (and is populated by the Live Agent engine automatically):

Now you can safely configure your dialog and be sure to know if the user is logged or not, and change the Bot behavior consequently:

Powered by WordPress & Theme by Anders Norén