Skip to main content

3. Security, Permission Management, Chat Rooms, and Temporary Conversations

Introduction

In the previous chapter, Advanced Messaging Features, Push Notifications, Synchronization, and Multi-Device Sign-on, we introduced a number of bonus features that you can implement beyond basic messaging. In this chapter, we will introduce more features from the perspectives of system security and permission management, including:

  • How to verify the requests made by clients with a third-party signing mechanism
  • How to control the permissions each user has
  • How to build a chat room with an unlimited number of people
  • How to enforce text moderation on the messages sent by users
  • How to implement temporary conversations

Signing Mechanism

Instant Messaging is decoupled from the account system offered by Data Storage. This makes it possible for you to use Instant Messaging even though the account system of your app is not built with Data Storage. To ensure the security of your app, we offer a third-party signing mechanism that helps your app verify all the requests sent from clients.

The mechanism comes with an authentication server (the so-called "third party") deployed between clients and the cloud. Each time a client wants to make a request involving sensitive operations (like logging in, creating conversations, joining conversations, or inviting users), it has to obtain a signature from the authentication server. The signature gets attached to the request and will be verified by the cloud according to a predefined protocol. Only those requests with valid signatures will be accepted by the cloud.

The signing mechanism is turned off by default. You can turn it on by going to Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings:

  • Verify signatures for logging in: Verify all the activities of logging in
  • Verify signatures for conversation operations: Verify all the activities of creating conversations, joining conversations, inviting users, and removing users
  • Verify signatures for retrieving history messages: Verify all the activities of retrieving history messages
  • Verify signatures for blacklist operations: Verify all the activities of changing blacklisted users of conversations (see the next section for more details regarding blacklists)

You are free to change the settings here based on your app's actual needs, though we highly recommend that you at least keep verifying signatures for logging in on, which guarantees the basic security of your app.

sequenceDiagram Client->>Authentication Server: 1. Apply for signature with request Authentication Server-->>Client: 2. Return timestamp, nonce, and signature to the client Client->>Authentication Server: 3. Send the request to the cloud with the signature Authentication Server-->>Client: 4. Verify the signature along with the request
  1. When the client performs operations like logging in or creating conversations, the SDK applies for a signature by calling SignatureFactory with the user's information and a request containing the operations to be done.
  2. The authentication server checks if the operations are performed with enough permissions. If that's true, the server will follow the signing algorithm that will be mentioned later to generate the timestamp, nonce, and signature, and send them back to the client.
  3. The client attaches the signature to the request and sends them to the cloud.
  4. The cloud verifies the signature along with the request to ensure that the operations in the request are allowed. The request will be accepted only if the signature is valid.

The algorithm used for the signing process is HMAC-SHA1 and the output would be the hex dump of a byte stream. For different requests, different strings with different UTC timestamps and nonces need to be constructed.

If you are using LCUser in your app, you can get signatures for logging in through our REST API.

Formats of Signatures

Below we will introduce the formats of strings used to obtain signatures for different types of operations.

Signatures for Logging in

Below is the format of strings for logging in. Notice that there are two colons between clientid and timestamp:

appid:clientid::timestamp:nonce
ParameterDescription
appidYour App ID.
clientidThe clientId used for logging in.
timestampThe number of milliseconds that have elapsed since Unix epoch (UTC).
nonceA random string.

Note: The key for signing has to be the Master Key of your app. You can find it from Developer Center > Your game > Game Services > Configuration. Make sure your Master Key is well-protected and doesn't get leaked.

You may implement your own SignatureFactory to retrieve signatures from remote servers. If you don't have your own server, you may use the web hosting service provided by Cloud Engine. Generating signatures within your mobile app is extremely dangerous since your Master Key can get exposed.

This signature expires in 6 hours, but it becomes invalid immediately once the client has been forced to log out. The signature invalidness does not affect the currently connected clients.

Signatures for Creating Conversations

Below is the format of strings for creating conversations:

appid:clientid:sorted_member_ids:timestamp:nonce
  • appid, clientid, timestamp, and nonce are the same as above.
  • sorted_member_ids is a list of clientIds (users being invited to the conversation) arranged in ascending order and divided by colon (:).

Signatures for Group Operations

Below is the format of strings for joining conversations, inviting users, and removing users:

appid:clientid:convid:sorted_member_ids:timestamp:nonce:action
  • appid, clientid, sorted_member_ids, timestamp, and nonce are the same as above. sorted_member_ids would be an empty string if you are creating a new conversation.
  • convid is the conversation ID.
  • action is the operation being performed: invite means joining a conversation or inviting users and kick means removing users.

Signatures for Retrieving Message Histories

appid:client_id:convid:nonce:timestamp

The meanings of these parameters are the same as above.

This signature is only used for REST API. It is not applicable to client-side SDKs.

Signatures for Blacklist Operations

There are two formats of strings for two types of blacklist operations:

  1. client to conversation
appid:clientid:convid::timestamp:nonce:action
  • action is the operation being performed: client-block-conversations means blocking the conversation and client-unblock-conversations means unblocking the conversation.
  1. conversation to client
appid:clientid:convid:sorted_member_ids:timestamp:nonce:action
  • action is the operation being performed: conversation-block-clients means blocking the client and conversation-unblock-clients means unblocking the client.
  • sorted_member_ids is the same as above.

Demo for Generating Signatures on Cloud Engine

To help you better understand the signing algorithm, we made a server-side signing program based on Node.js and Cloud Engine. It's available here for you to study and use.

Supporting Signatures on the Client Side

So far we have been talking about the protocol used by the authentication server to generate signatures. Now let's see what we need to do with the client side to make the entire signing mechanism work.

The SDK reserves a factory interface Signature for each AVIMClient instance. To enable signing, implement the interface with a class that calls the signing method on the authentication server to get signatures, and then bind the class to the AVIMClient instance:

public class LocalSignatureFactory : ILCIMSignatureFactory {
const string MasterKey = "pyvbNSh5jXsuFQ3C8EgnIdhw";

public Task<LCIMSignature> CreateConnectSignature(string clientId) {
long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = NewNonce();
string signature = GenerateSignature(LCApplication.AppId, clientId, string.Empty, timestamp.ToString(), nonce);
return Task.FromResult(new LCIMSignature {
Signature = signature,
Timestamp = timestamp,
Nonce = nonce
});
}

public Task<LCIMSignature> CreateStartConversationSignature(string clientId, IEnumerable<string> memberIds) {
string sortedMemberIds = string.Empty;
if (memberIds != null) {
List<string> sortedMemberList = memberIds.ToList();
sortedMemberList.Sort();
sortedMemberIds = string.Join(":", sortedMemberList);
}
long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = NewNonce();
string signature = GenerateSignature(LCApplication.AppId, clientId, sortedMemberIds, timestamp.ToString(), nonce);
return Task.FromResult(new LCIMSignature {
Signature = signature,
Timestamp = timestamp,
Nonce = nonce
});
}

public Task<LCIMSignature> CreateConversationSignature(string conversationId, string clientId, IEnumerable<string> memberIds, string action) {
string sortedMemberIds = string.Empty;
if (memberIds != null) {
List<string> sortedMemberList = memberIds.ToList();
sortedMemberList.Sort();
sortedMemberIds = string.Join(":", sortedMemberList);
}
long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = NewNonce();
string signature = GenerateSignature(LCApplication.AppId, clientId, conversationId, sortedMemberIds, timestamp.ToString(), nonce, action);
return Task.FromResult(new LCIMSignature {
Signature = signature,
Timestamp = timestamp,
Nonce = nonce
});
}

public Task<LCIMSignature> CreateBlacklistSignature(string conversationId, string clientId, IEnumerable<string> memberIds, string action) {
string sortedMemberIds = string.Empty;
if (memberIds != null) {
List<string> sortedMemberList = memberIds.ToList();
sortedMemberList.Sort();
sortedMemberIds = string.Join(":", sortedMemberList);
}
long timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = NewNonce();
string signature = GenerateSignature(LCApplication.AppId, clientId, conversationId, sortedMemberIds, timestamp.ToString(), nonce, action);
return Task.FromResult(new LCIMSignature {
Signature = signature,
Timestamp = timestamp,
Nonce = nonce
});
}

private static string SignSHA1(string key, string text) {
HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(key));
byte[] bytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(text));
string signature = BitConverter.ToString(bytes).Replace("-", string.Empty);
return signature;
}

private static string NewNonce() {
byte[] bytes = new byte[10];
using (RandomNumberGenerator generator = RandomNumberGenerator.Create()) {
generator.GetBytes(bytes);
}
return Convert.ToBase64String(bytes);
}

private static string GenerateSignature(params string[] args) {
string text = string.Join(":", args);
string signature = SignSHA1(MasterKey, text);
return signature;
}
}

// Specify the signature factory
LCIMClient tom = new LCIMClient("tom", signatureFactory: new LocalSignatureFactory());

You should never perform signing using your Master Key on the client side. If your Master Key gets leaked, the data in your app would be accessible by anyone who has the key. Therefore, we highly recommend that you host the signing program on a server that is well-secured (like Cloud Engine).

Signing Mechanism for User

User is the built-in account system coming with Data Storage. If your users have their accounts signed up or logged in with User, they can skip the signing process when logging in to Instant Messaging. The code below shows how a user can log in to Instant Messaging with User:

LCUser user = await LCUser.Login("username", "password");
CIMClient client = new LCIMClient(user);
await client.Open();

When creating IMClient with an LCUser instance that has completed the logIn process, the user's signature information can be directly accessed by Instant Messaging from the account system. This allows Instant Messaging to automatically verify the client being logged in and the process of applying for signatures from the third-party server can be skipped.

Once IMClient is logged in, all the other features work in the same way as discussed earlier.

Permission Management and Blacklisting

The third-party signing mechanism helps to maintain the general security of your app, but each conversation still needs to keep its own order. For example, a chat room may need managers that can temporarily or permanently mute users that are behaving improperly. In this section, we will talk about how permission management within conversations can be implemented.

Setting Member Permissions

When permission management is enabled, members in each conversation will be divided into different roles with different permissions. To enable permission management, go to Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings and turn on Enable permission management for conversations.

Here is a table showing the permissions each role has:

RolePermissions
OwnerMute members, remove members, invite members, blacklist members, and update other members' permissions
ManagerMute members, remove members, invite members, blacklist members, and update other members' permissions
MemberJoin conversations

Among all these roles, Owner has the highest permissions and Member has the lowest. A member with higher permissions can change the role of a member with lower permissions, but not vice versa. In the previous chapters, we have seen that all the members in a conversation can invite or remove people, but once permission management is enabled, only Owner and Manager can perform these operations. Other members will get an error when attempting to do so.

The Owner of a conversation cannot be changed. For other members, their roles can be switched between Manager and Member with Conversation#updateMemberRole:

/// <summary>
/// Updates the role of a member of this conversation.
/// </summary>
/// <param name="memberId">The member to update.</param>
/// <param name="role">The new role of the member.</param>
/// <returns></returns>
public async Task UpdateMemberRole(string memberId, string role);

Getting Member Permissions

A Conversation object offers two ways for getting permission information of members:

Get all members' permission information

/// <summary>
/// Gets all member roles.
/// </summary>
/// <returns></returns>
public async Task<ReadOnlyCollection<LCIMConversationMemberInfo>> GetAllMemberInfo();

Get a specific member's permission information

/// <summary>
/// Gets the role of a specific member.
/// </summary>
/// <param name="memberId">The member to query.</param>
/// <returns></returns>
public async Task<LCIMConversationMemberInfo> GetMemberInfo(string memberId);

Each return value contains permission information of members in a triple or array <ConversationId, MemberId, ConversationMemberRole>.

Muting Members

Members whose roles are Owner or Manager can mute other members so they can only receive messages from the conversation. They will get an error when they attempt to send a message.

LCIMConversation offers the following methods related to muting members:

/// <summary>
/// Mutes members of this conversation.
/// </summary>
/// <param name="clientIds">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> MuteMembers(IEnumerable<string> clientIds);
/// <summary>
/// Unmutes members of this conversation.
/// </summary>
/// <param name="clientIdList">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> UnmuteMembers(IEnumerable<string> clientIds);
/// <summary>
/// Queries muted members.
/// </summary>
/// <param name="limit">Limits the number of returned results.</param>
/// <param name="next">Can be used for pagination with the limit parameter.</param>
/// <returns></returns>
public async Task<LCIMPageResult> QueryMutedMembers(int limit = 10, string next = null);

Note that the result of the operation contains three parts of data:

  • error/exception: Whether the operation is holistically successful. If false, you will get error messages from here and the following two parts can be ignored.
  • successfulClientIds: The clientIds that are operated successfully.
  • failedIds: The failures occurred and the clientIds associated with each of them; listed in the format of List<ReasonString, List<ClientId>>.

Events for Muting Members

All the members in the conversation will receive notifications when someone gets muted.

Blacklisting

There are two types of blacklists available:

  • Conversation to user: The list of users that are blocked by a conversation. Blocked users cannot join the conversation.
  • User to conversation: The list of conversations that are blocked by a user. The user cannot be invited to a blocked conversation.

To enable blacklists, go to Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings > Instant Messaging settings and turn on Enable blacklists.

The LCIMConversation class offers the following methods related to blacklisting:

/// <summary>
/// Adds members to the blocklist of this conversation.
/// </summary>
/// <param name="clientIds">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> BlockMembers(IEnumerable<string> clientIds);
/// <summary>
/// Removes members from the blocklist of this conversation.
/// </summary>
/// <param name="clientIds">Member list.</param>
/// <returns></returns>
public async Task<LCIMPartiallySuccessResult> UnblockMembers(IEnumerable<string> clientIds);
/// <summary>
/// Queries blocked members.
/// </summary>
/// <param name="limit">Limits the number of returned results.</param>
/// <param name="next">Can be used for pagination with the limit parameter.</param>
/// <returns></returns>
public async Task<LCIMPageResult> QueryBlockedMembers(int limit = 10, string next = null);

The result of the operation is similar to that for muting members. You get the clientIds that are operated successfully, plus the failures occurred and the clientIds associated with each of them.

Once a user is added to the blacklist of a conversation, the user will be removed from the conversation and cannot receive messages from it anymore. Unless the user is removed from the blacklist, other members will not be able to add this user back to the conversation.

Events for Blacklisting

All the members in the conversation will receive notifications when someone gets blacklisted.

Blocking Messages from Specific Users

Another scenario is that a user doesn't want to receive messages from a specific user. This can be implemented with hooks. See Hooks for more details.

Chat Rooms

We have compared different types of scenarios and conversations in our service overview. Now let's learn how to build a chat room.

Creating Chat Rooms

IMClient has the createChatRoom method for creating chat rooms:

// Pass in the name of the chat room
tom.CreateChatRoom("Chat Room");

When creating a chat room, you can specify its name and optional attributes. The interface for creating chat rooms has the following differences compared to that for creating basic conversations:

  • A chat room doesn't have a member list, so there is no need to specify members.
  • For the same reason, there is no need to specify unique (the cloud doesn't need to merge conversations by member lists).

Although it's possible to create a chat room by passing { transient: true } into createConversation, we still recommend that you use createChatRoom directly.

Finding Chat Rooms

In the first chapter, we have discussed how you can use ConversationsQuery to look for conversations with your custom conditions. This works for chat rooms as well, as long as you add transient = true as a constraint.

LCIMConversationQuery query = new LCIMConversationQuery(tom);
query.WhereEqualTo("tr", true);

The Java (Android) SDK offers the LCIMClient#getChatRoomQuery method that is dedicated to querying chat rooms. By using this method, you won't need to deal with the transient attribute of conversations.

Joining and Leaving Chat Rooms

When coming to the interfaces for joining or leaving conversations, group chats are the same as basic conversations. See Group Chats in the first chapter for more details.

However, there are several differences in the ways members are managed and notifications are delivered:

  • A user cannot be invited to or removed from a chat room. They are only able to join or leave on their own.
  • If a user logs out, this user will be automatically removed from the chat room they are already in. An exception is that if the user gets offline unexpectedly, they will be added back to the chat room they are previously in as long as they get back within 30 minutes.
  • The cloud will not deliver notifications for users joining or leaving chat rooms.
  • The list of members in a chat room cannot be retrieved. Only the count of members is available.

As a side note, functions like push notifications, message synchronization, and receipts are also not supported by chat rooms.

Getting Member Counts

The LCIMConversation#memberCount method lets you get the count of members in a conversation. When used on a chat room, you get the number of people in it at that moment:

int membersCount = await conversation.GetMembersCount();

Message Priorities

To ensure that important messages get delivered promptly, the server would selectively discard a certain amount of messages with lower priorities when the network connection is bad. Below are the priorities supported:

PriorityDescription
MessagePriority.HIGHHigh priority. Used for messages that need to be delivered promptly.
MessagePriority.NORMALNormal priority. Used for ordinary text messages.
MessagePriority.LOWLow priority. Used for messages that are less important.

The default priority is NORMAL.

The priority of a message can be set when sending the message. The code below shows how you can send a message with high priority:

LCIMTextMessage message = new LCIMTextMessage("The score is still 0:0. China definitely needs a substitution for the second half.");
LCIMMessageSendOptions options = new LCIMMessageSendOptions {
Priority = LCIMMessagePriority.High
};
await chatRoom.Send(message, options);

Note:

This feature is only available for chat rooms. There won't be an effect if you set priorities for messages in basic conversations, since these messages will never get discarded.

Muting Conversations

If a user doesn't want to get notifications for new messages in a conversation but still wants to stay in the conversation, they can mute the conversation.

For example, Tom is getting busy and wants to mute a conversation:

await chatRoom.Mute();

After a conversation is muted, the current user will not get push notifications from it anymore. To unmute a conversation, use Conversation#unmute.

Tips:

  • Both chat rooms and basic conversations can be muted.
  • mute and unmute operations will change the mu field in the _Conversation class. Do not change the mu field directly in your app's dashboard, otherwise push notifications may not work properly.

Text Moderation

Instant Messaging offers a built-in text moderation function that allows you to filter cuss words out from the messages sent by users. You can enable it for one-on-one chats by going to Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings.

Matched keywords will be replaced with ***.

Text moderation will lead to message modifications at the system level, so the message sender will receive a MESSAGE_UPDATE event, which the clients can listen to. Please refer to the "Modify a Message" section of the previous chapter for code samples.

If you have upgraded to the Business Plan, you can customize the keywords. To do so, go to Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings and upload your keywords file. The uploaded file must be UTF-8 encoded with one keyword in each line. For now, the Enable sensitive keyword filtering against group chats option on Developer Center > Your game > Game Services > Cloud Services > Instant Messaging > Settings is always enabled, but if you don't upload a file of keywords, the text moderation function will not function in the end.

Filtering rules for sensitive words: If the message is a rich media message type (the _lctype attribute has a value), only the contents of the _lctext field are filtered. If the message is not a rich media message type (the message does not have the _lctype attribute), then the entire message body is filtered.

If you have more complicated requirements regarding text moderation, we recommend that you make use of the _messageReceived hook of Cloud Engine. You can define your own logic for controlling messages.

Temporary Conversations

Temporary conversations can be used for special scenarios with:

  • Short TTL
  • Fewer members (10 clientIds maximum)
  • No message history needed

What makes temporary conversations different from other conversations is that they expire very quickly. This helps you reduce the space needed for storing conversations and lower the cost of maintaining your app. Temporary conversations are best used for customer service systems.

Creating Temporary Conversations

IMConversation has its createTemporaryConversation method for creating temporary conversations:

LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" });

Temporary conversations have an important attribute that differentiates them from others: TTL. It is set to 1 day by default, but you can change it to any time no longer than 30 days. If you want a conversation to survive for more than 30 days, make it a basic conversation instead. The code below creates a temporary conversation with a custom TTL:

LCIMTemporaryConversation temporaryConversation = await tom.CreateTemporaryConversation(new string[] { "Jerry", "William" },
ttl: 3600);

Besides this, a temporary conversation shares the same functionality as a basic conversation.

Continue Reading