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" },
})
);
// create chat session
const session = connect.ChatSession.create({
chatDetails: {
// put an invalid contact ID
contactId: "invalid contact id",
participantId: ParticipantId,
participantToken: ParticipantToken,
},
options: { region: "us-east-1" },
type: connect.ChatSession.SessionTypes.CUSTOMER,
});
// subscribe to message events
session.onMessage((event) => {
if (event.data.Type === "MESSAGE") {
console.log("Received message:", event.data.Content);
}
});
session.onConnectionEstablished(async (event) => {
// ensure the WebSocket connection has been established before sending the message
// see: https://github.com/amazon-connect/amazon-connect-chatjs/issues/124
if (event.data) {
// send a message
await session.sendMessage({
contentType: "text/plain",
message: "Hello World!",
});
}
});
// connect to the chat
await session.connect();
Expected result:
The Hello World! message is sent and the following is logged:
Received message: Hello World!
Actual result:
The Hello World! message is sent, but nothing gets logged (i.e. the onMessage() handler is not invoked).
Analysis:
The contactId specified in ChatSession.create() is not validated to ensure it's associated with the specified participantToken. However, it's used to filter 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; |
|
connect(args={}) { |
|
this.sessionMetadata = args.metadata || null; |
|
this.argsValidator.validateConnectChat(args); |
|
const connectionDetailsProvider = this._getConnectionDetailsProvider(); |
|
return connectionDetailsProvider.fetchConnectionDetails() |
|
.then( |
|
(connectionDetails) => |
|
this._initConnectionHelper(connectionDetailsProvider, connectionDetails) |
|
_initConnectionHelper(connectionDetailsProvider, connectionDetails) { |
|
this.connectionHelper = new LpcConnectionHelper( |
|
this.contactId, |
|
this.initialContactId, |
|
connectionDetailsProvider, |
|
this.websocketManager, |
|
this.logMetaData, |
|
connectionDetails |
|
); |
|
this.connectionHelper.onEnded(this._handleEndedConnection.bind(this)); |
|
this.connectionHelper.onConnectionLost(this._handleLostConnection.bind(this)); |
|
this.connectionHelper.onConnectionGain(this._handleGainedConnection.bind(this)); |
|
this.connectionHelper.onMessage(this._handleIncomingMessage.bind(this)); |
|
class LpcConnectionHelper extends BaseConnectionHelper { |
|
|
|
constructor(contactId, initialContactId, connectionDetailsProvider, websocketManager, logMetaData, connectionDetails) { |
|
super(connectionDetailsProvider, logMetaData); |
|
|
|
// WebsocketManager instance is only provided iff agent connections |
|
this.customerConnection = !websocketManager; |
|
|
|
if (this.customerConnection) { |
|
// ensure customer base instance exists for this contact ID |
|
if (!LpcConnectionHelper.customerBaseInstances[contactId]) { |
|
LpcConnectionHelper.customerBaseInstances[contactId] = |
|
new LpcConnectionHelperBase(connectionDetailsProvider, undefined, logMetaData, connectionDetails); |
|
} |
|
this.baseInstance = LpcConnectionHelper.customerBaseInstances[contactId]; |
|
} else { |
|
// cleanup agent base instance if it exists for old websocket manager |
|
if (LpcConnectionHelper.agentBaseInstance) { |
|
if (LpcConnectionHelper.agentBaseInstance.getWebsocketManager() !== websocketManager) { |
|
LpcConnectionHelper.agentBaseInstance.end(); |
|
LpcConnectionHelper.agentBaseInstance = null; |
|
} |
|
} |
|
// ensure agent base instance exists |
|
if (!LpcConnectionHelper.agentBaseInstance) { |
|
LpcConnectionHelper.agentBaseInstance = |
|
new LpcConnectionHelperBase(undefined, websocketManager, logMetaData); |
|
} |
|
this.baseInstance = LpcConnectionHelper.agentBaseInstance; |
|
} |
|
|
|
this.contactId = contactId; |
|
this.initialContactId = initialContactId; |
|
this.status = null; |
|
this.eventBus = new EventBus(); |
|
this.subscriptions = [ |
|
this.baseInstance.onEnded(this.handleEnded.bind(this)), |
|
this.baseInstance.onConnectionGain(this.handleConnectionGain.bind(this)), |
|
this.baseInstance.onConnectionLost(this.handleConnectionLost.bind(this)), |
|
this.baseInstance.onMessage(this.handleMessage.bind(this)) |
|
]; |
|
} |
Ultimately, when a message is received, it's filtered based on the contactId or initialContact:
|
handleMessage(message) { |
|
if (message.InitialContactId === this.initialContactId || message.ContactId === this.contactId || message.Type === CHAT_EVENTS.MESSAGE_METADATA) { |
|
this.eventBus.trigger(ConnectionHelperEvents.IncomingMessage, message); |
|
} |
|
} |
Thus, this results in events not being triggered when messages are received through the WebSocket (including onMessageEvent()).
Proposed fix:
Modify ChatController.prototype.connect() to validate that the specified contactId is associated with the specified participantToken and throw an Error otherwise.
In order to do the validation the connectparticipant:GetTranscript operation could be used.
Steps to reproduce:
Expected result:
The
Hello World!message is sent and the following is logged:Actual result:
The
Hello World!message is sent, but nothing gets logged (i.e. theonMessage()handler is not invoked).Analysis:
The
contactIdspecified inChatSession.create()is not validated to ensure it's associated with the specifiedparticipantToken. However, it's used to filter messages received from the underlyingWebSocketManager:amazon-connect-chatjs/src/core/chatSession.js
Line 216 in d85c517
amazon-connect-chatjs/src/core/chatSession.js
Lines 198 to 209 in d85c517
amazon-connect-chatjs/src/core/chatSession.js
Lines 37 to 38 in d85c517
amazon-connect-chatjs/src/core/chatSession.js
Lines 52 to 70 in d85c517
amazon-connect-chatjs/src/core/chatArgsValidator.js
Lines 88 to 100 in d85c517
amazon-connect-chatjs/src/core/chatArgsValidator.js
Lines 53 to 82 in d85c517
amazon-connect-chatjs/src/core/chatController.js
Lines 32 to 44 in d85c517
amazon-connect-chatjs/src/core/chatController.js
Lines 192 to 199 in d85c517
amazon-connect-chatjs/src/core/chatController.js
Lines 207 to 219 in d85c517
amazon-connect-chatjs/src/core/connectionHelpers/LpcConnectionHelper.js
Lines 12 to 53 in d85c517
Ultimately, when a message is received, it's filtered based on the
contactIdorinitialContact:amazon-connect-chatjs/src/core/connectionHelpers/LpcConnectionHelper.js
Lines 107 to 111 in d85c517
Thus, this results in events not being triggered when messages are received through the
WebSocket(includingonMessageEvent()).Proposed fix:
Modify
ChatController.prototype.connect()to validate that the specifiedcontactIdis associated with the specifiedparticipantTokenand throw anErrorotherwise.In order to do the validation the connectparticipant:GetTranscript operation could be used.