Skip to content

Specifying incorrect contactId in ChatSession.create() fails silently, events are not invoked #136

Description

@marcogrcr

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions