Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Specifying incorrect participantId in ChatSession.create() fails silently, causes onDeliveredReceipt()/onReadReceipt() listeners to be invoked when they shouldn't #137

Open
marcogrcr opened this issue Feb 25, 2023 · 0 comments
Labels
🗒️ In Backlog Reviewed by team, added to backlog

Comments

@marcogrcr
Copy link
Contributor

Steps to reproduce:

import "amazon-connect-chatjs"; // v1.3.1
import {
  ConnectClient,
  StartChatContactCommand,
} from "@aws-sdk/client-connect"; // v3.254.0

// start a chat contact
const client = new ConnectClient({
  region: "us-east-1",
  credentials: {
    accessKeyId: "...",
    secretAccessKey: "...",
    sessionToken: "...",
  },
});

const { ContactId, ParticipantId, ParticipantToken } = await client.send(
  new StartChatContactCommand({
    InstanceId: "...",
    ContactFlowId: "...",
    ParticipantDetails: { DisplayName: "Customer" },
  })
);

// set global config (enable delivery/read receipts)
connect.ChatSession.setGlobalConfig({});

// create chat session
const session = connect.ChatSession.create({
  chatDetails: {
    contactId: ContactId,
    // put an invalid participant ID
    participantId: "invalid participant id",
    participantToken: ParticipantToken,
  },
  options: { region: "us-east-1" },
  type: connect.ChatSession.SessionTypes.CUSTOMER,
});

// subscribe to message events
session.onMessage(async (event) => {
  if (event.data.Type === "MESSAGE") {
    // send read receipt
    const { Id: messageId } = event.data;
    await session.sendEvent({
      contentType: "application/vnd.amazonaws.connect.event.message.read",
      content: JSON.stringify({ messageId }),
    });
    console.log("Sent read receipt.");
  }
});

// subscribe to read receipts
session.onReadReceipt((event) => {
  console.log("Received read receipt:", event.data);
});

// connect to the chat
await session.connect();

Expected result:

The read receipt is sent and the onReadReceipt() is not invoked.

Actual result:

The read receipt is sent and the onReadReceipt() is invoked:

Sent read receipt.

Received read receipt: {
    "AbsoluteTime": "(...some date...)",
    "ContentType": "application/vnd.amazonaws.connect.event.message.metadata",
    "Id": "(...some id...)",
    "Type": "MESSAGEMETADATA",
    "MessageMetadata": {
        "MessageId": "(... some message id...)",
        "Receipts": [
            {
                "DeliveredTimestamp": "(...some date...)",
                "ReadTimestamp": "(...some date...)",
                "RecipientParticipantId": "(...some participant id...)"
            }
        ]
    }
}

Analysis:

The participantId specified in ChatSession.create() is not validated to ensure it's associated with the specified participantToken. However, it's used to filter receipt messages received from the underlying WebSocketManager:

create: ChatSessionConstructor,

var ChatSessionConstructor = args => {
var options = args.options || {};
var type = args.type || SESSION_TYPES.AGENT;
GlobalConfig.updateStageRegion(options);
// initialize CSM Service for only customer chat widget
// Disable CSM service from canary test
if(!args.disableCSM && type === SESSION_TYPES.CUSTOMER) {
csmService.loadCsmScriptAndExecute();
}
return CHAT_SESSION_FACTORY.createChatSession(
type,
args.chatDetails,

createChatSession(sessionType, chatDetails, options, websocketManager) {
const chatController = this._createChatController(sessionType, chatDetails, options, websocketManager);

_createChatController(sessionType, chatDetailsInput, options, websocketManager) {
var chatDetails = this.argsValidator.normalizeChatDetails(chatDetailsInput);
var logMetaData = {
contactId: chatDetails.contactId,
participantId: chatDetails.participantId,
sessionType
};
var chatClient = ChatClientFactory.getCachedClient(options, logMetaData);
var args = {
sessionType: sessionType,
chatDetails,
chatClient,
websocketManager: websocketManager,
logMetaData,
};
return new ChatController(args);

normalizeChatDetails(chatDetailsInput) {
let chatDetails = {};
chatDetails.contactId = chatDetailsInput.ContactId || chatDetailsInput.contactId;
chatDetails.participantId = chatDetailsInput.ParticipantId || chatDetailsInput.participantId;
chatDetails.initialContactId = chatDetailsInput.InitialContactId || chatDetailsInput.initialContactId
|| chatDetails.contactId || chatDetails.ContactId;
chatDetails.getConnectionToken = chatDetailsInput.getConnectionToken || chatDetailsInput.GetConnectionToken;
if (chatDetailsInput.participantToken || chatDetailsInput.ParticipantToken) {
chatDetails.participantToken = chatDetailsInput.ParticipantToken || chatDetailsInput.participantToken;
}
this.validateChatDetails(chatDetails);
return chatDetails;
}

validateChatDetails(chatDetails, sessionType) {
Utils.assertIsObject(chatDetails, "chatDetails");
if (sessionType===SESSION_TYPES.AGENT && !Utils.isFunction(chatDetails.getConnectionToken)) {
throw new IllegalArgumentException(
"getConnectionToken was not a function",
chatDetails.getConnectionToken
);
}
Utils.assertIsNonEmptyString(
chatDetails.contactId,
"chatDetails.contactId"
);
Utils.assertIsNonEmptyString(
chatDetails.participantId,
"chatDetails.participantId"
);
if (sessionType===SESSION_TYPES.CUSTOMER){
if (chatDetails.participantToken){
Utils.assertIsNonEmptyString(
chatDetails.participantToken,
"chatDetails.participantToken"
);
} else {
throw new IllegalArgumentException(
"participantToken was not provided for a customer session type",
chatDetails.participantToken
);
}
}
}

class ChatController {
constructor(args) {
this.argsValidator = new ChatServiceArgsValidator();
this.pubsub = new EventBus();
this.sessionType = args.sessionType;
this.getConnectionToken = args.chatDetails.getConnectionToken;
this.connectionDetails = args.chatDetails.connectionDetails;
this.initialContactId = args.chatDetails.initialContactId;
this.contactId = args.chatDetails.contactId;
this.participantId = args.chatDetails.participantId;
this.chatClient = args.chatClient;
this.participantToken = args.chatDetails.participantToken;

Ultimately, when a receipt message is received, it's filtered based on the participantId:

_handleIncomingMessage(incomingData) {
try {
let eventType = getEventTypeFromContentType(incomingData?.ContentType);
if (this.messageReceiptUtil.isMessageReceipt(eventType, incomingData)) {
eventType = this.messageReceiptUtil.getEventTypeFromMessageMetaData(incomingData?.MessageMetadata);
if (!eventType ||
!this.messageReceiptUtil.shouldShowMessageReceiptForCurrentParticipantId(this.participantId, incomingData)) {
//ignore bec we do not want to show messageReceipt to sender of receipt.
//messageReceipt needs to be shown to the sender of message.
return;
}
}

isMessageReceipt(eventType, incomingData) {
return [CHAT_EVENTS.INCOMING_READ_RECEIPT, CHAT_EVENTS.INCOMING_DELIVERED_RECEIPT]
.indexOf(eventType) !== -1 || incomingData.Type === CHAT_EVENTS.MESSAGE_METADATA;
}

shouldShowMessageReceiptForCurrentParticipantId(currentParticipantId, incomingData) {
const recipientParticipantId = incomingData.MessageMetadata &&
Array.isArray(incomingData.MessageMetadata.Receipts) &&
incomingData.MessageMetadata.Receipts[0] &&
incomingData.MessageMetadata.Receipts[0].RecipientParticipantId;
return currentParticipantId !== recipientParticipantId;
}

Thus, this results in onDeliveredReceipt()/onReadReceipt() event listeners being triggered for receipts sent by oneself because the participantId does not match.

Proposed fix:

Modify ChatController.prototype.connect() to validate that the specified participantId is associated with the specified participantToken and throw an Error otherwise.

In order to do the validation the connectparticipant:GetTranscript operation could be used.

@spencerlepine spencerlepine added the ⌛️ Needs Triage Needs team review label Apr 27, 2023
@spencerlepine spencerlepine added 🗒️ In Backlog Reviewed by team, added to backlog and removed ⌛️ Needs Triage Needs team review labels May 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🗒️ In Backlog Reviewed by team, added to backlog
Projects
None yet
Development

No branches or pull requests

2 participants