WebWebNode.jsPhoneGapReact V4JavaScriptBuilding a Secure Chat App in JavaScript V4

Use cases based guide with PubNub APIs

The purpose of this guide is to provide developer-friendly instruction on building a chat app using PubNub's JavaScript V4 SDK. It includes use case-based code samples, recommendations on best practices, and common design patterns.

Some APIs used in this documentation are add-on features, which need to be enabled via the Developer Admin Dashboard.

To get started, you must register your app on your Admin dashboard page to obtain your subscribeKey and publishKey.

You must include the PubNub JavaScript SDK in your code before initializing the client.

<script src="https://cdn.pubnub.com/sdk/javascript/pubnub.4.21.2.js"></script>

Next, in your JavaScript V4 file, instantiate a new Pubnub instance:

var pubnub = new PubNub({
	subscribeKey: "mySubscribeKey",
	publishKey: "myPublishKey",
	ssl: true
})

Joining a chat room can be done by using subscribe() to listen on a channel, room-1:

pubnub.subscribe({
	channels: ['room-1']
})

You can easily create a private 1:1 chat by generating a unique arbitrary channel name (e.g. private-36258a4) and sharing it with only 2 people to avoid the room name being easily duplicated by accident.

 
You need to enable Stream Controller from your Admin Dashboard.

Subscribe to multiple channels, such as room-1, room-2... on a single connection (Multiplexing).

With channel multiplexing, a client can subscribe to many channels via a single socket connection. The PubNub Data Stream Network will the list on the server so that the client will receive messages from all subscribed channels.

pubnub.subscribe({
    channels: ['room-1', 'room-2', 'room-3'],
    withPresence: true // also subscribe to presence instances.
})

Use unsubscribe() to leave the channel:

pubnub.unsubscribe({
	channels: ['room-1']
})

Subscribing is an async operation- upon the callback of message inside the subscribe operation, the client will receive all messages that have been published to the channel. The message callback is called every time a set of data is published:

pubnub.subscribe({
	channels: ['room-1']
})

where m is an object that contains the message you received.

Using publish() allows you to send messages as an object:

pubnub.publish(
	{
		message: {
			avatar: 'grumpy-cat.jpg',
			text: 'Hello, hoomans!'
		},
		channel: 'room-1'
	},
	function (status, response) {
		// handle status, response
	}
);

You can send any data (a string, JSON, or an object), as long as the data size is below 32k.

When an initialization is established, a UUID is automatically generated at init when you initialize with PubNub by default. Alternatively, you can create a unique ID for the user and include it as a uuid.

You can generate one manually either with uuid() method, or your own method.

The uuid can also be a unique string. For example, if your login method is a social login, such as Twitter OAuth, you can assign the username as uuid.

var newUUID = PubNub.generateUUID();

var pubnub = new PubNub ({ 
    uuid: newUUID, 
    subscribeKey: "mySubscribeKey",
    publishKey: "myPublishKey",
    secretKey: "secretKey"
});
 
For more details about setting UUID see PubNub Knowledge Base: How do I set a UUID.
 
You need to enable Presence from the Admin Dashboard.

Use the presence callback for subscribe to receive presence events, which includes occupancy, and state change such as leave, join, timeout, and state-change. (See Binding extra info (State) for each user).

pubnub.subscribe({
	channels: ['room-1'],
	withPresence: true
})
 
You need to enable Presence from your Admin Dashboard.
pubnub.subscribe({
	channels: ['room-1'],
	withPresence: true
})
where m.action returns either join (when a user subscribes to the channel), leave (when the user unsubscribes from the channel), timeout (when a user is disconnected). Also, state-change is triggered when the user's state data has been modified (see Binding extra info (State) for each user)
 
The action key doesn’t exist for presence interval (when you exceed presence announce max) and also not for active/inactive webhook events.
 
You need to enable Presence from your Admin Dashboard.

You can obtain information about the current state of a channel, including a list of unique user-ids currently subscribed to the channel, by calling the hereNow() function after the connection is established:

pubnub.hereNow(
	{
		channels: "room-1",
		includeUUIDs: true,
		includeState: true
	}, 
	function (status) {
		// handle state setting response
	}
);

The response includes m.uuid as an array of uuids as follows:

uuids : [
    'grumpy-cat', 
    'maru', 
    'lil-bab'
]

You can set state, extra information for a user, during a subscribe with the state parameter. Simply specify any desired key/value pairs as a JSON object.

pubnub.setState(
	{
		state: {
			mood : 'grumpy'
		},
		channels: ['room-1']
	},
	function (status) {
		// handle state setting response
	}
);

You can get the state info of a user at any time, too:

pubnub.getState(
	{
		channels: ['room-1']
	},
	function (status) {
		// handle state setting response
	}
);

The state API allows you to provide user state data, and the data is sent to other subscribers upon a join event.

The state can be an object of key/value pairs with any arbitrary data, as long as the data types are int, float, and string.

You can achieve this via state change.

You may want to listen to DOM events like keydown or keypress for a chat text field, and when the event is triggered, change the user's state to something like {isTyping : true}, then toggle it back to false as the user stopped typing or timeout, and notify all other subscribers, as shown in the previous example, Binding extra info (State) for each user.

 
You need to enable Stream Controller from your Admin Dashboard.

Early we reviewed how to join multiple room over a single socket connection by using a comma delimited list. However, managing large lists can be complex. Therefore you can use Channel Groups to group up to 2,000 channels under a single Channel group.

pubnub.channelGroups.addChannels(
	{
		channels: ['room-1'],
		channelGroup: "Group-Cats"
	},
	function (status, response) {
	}
);

To remove a channel from the group, use pubnub.channelGroups.removeChannels().

To subscribe to a channel group:

pubnub.subscribe({
	channelGroups: ['Group-Cats']
})
For more information on grouping, please refer to our Stream Controller API documentation

Each channel group and associated channel(s) is identified by a unique name. These names may be a string of up to 64 Unicode characters, excluding reserved characters: , , : , . , / , *, \, non-printable ASCII control characters, and Unicode zero.

For example, the channel group, Group-Cats might contain the following channel names: general, lolcats-1 lolcats-2, maru, nyancat-original, nyancat-variants, etc.

Although in this example, the group name is capitalized and channel names are all in lowercase, you are free to name channels whatever you want to as long as you stick with your own consistent naming convention.

 
You need to enable Storage & Playback from your Admin Dashboard.

You can fetch the historical messages have been sent to a channel using history().

pubnub.history(
	{
		channel: 'room-1',
		reverse: false, // true to send via post
		count: 50, // how many items to fetch
        stringifiedTimeToken: true // false is the default
	},
	function (status, response) {
		// handle status, response
	}
);

where m[0] outputs an array of 50 most recently sent messages on the given channel, room-1. By default, last 100 messages are returned if you don't specify a count.

100 is the maximum number of messages you can fetch by the API. If you need more than 100, see the section, Retrieving more than 100 messages from history.

 
You need to enable Storage & Playback from your Admin Dashboard.

To retrieve messages within certain time tokens, use start and end arguments with history():

pubnub.history(
    {
         channel: "room-1',
         count: 100, // how many items to fetch
         stringifiedTimeToken: true, // false is the default
         start: 13827485876355504, // start time token to fetch
         end: 123123123133 // end timetoken to fetch
    },
    function (status, response) {
        // handle status, response
    }
);

The time tokens must be in valid 17 digit tokens. To convert date objects to the specific time tokens, get dateObj.getTime() and multiply by 10000 :

var fiveMinAgo = (new Date().getTime() - (5*60*1000)) * 10000;

By default, history returns maximum 100 messages. To retrieve more than 100, use timestamp to page through the history.

getAllMessages = function(timetoken) {
    pubnub.history(
        {
            channel: 'history_test',
            stringifiedTimeToken: true, // false is the default
            start: timetoken // start time token to fetch
        },
        function (status, response) {
            var msgs = response.messages;
            var start = response.startTimeToken;
            var end = response.endTimeToken;
            // if msgs were retrieved, do something useful with them
            if (msgs != undefined && msgs.length > 0) {
                console.log(msgs.length);
                console.log("start : " + start);
                console.log("end : " + end);
            }
            // if 100 msgs were retrieved, there might be more; call history again
            if (msgs.length == 100)
                getAllMessages(start);
        }
    );
}


//Usage examples:
//getAllMessages();
//getAllMessages(null);

Or you can use the wrapper, pubnub-flex-history to handle it automatically: https://github.com/pubnub/pubnub-flex-history

PubNub Access Manager allows you to manage granular permissions for your realtime apps and data. For example, you can use the Access Manager to create a secure private channel for a subset of users only.

 
You need to enable Access Manager from your Admin Dashboard.
 
If you are using PubNub JavaScript SDK version 3.6 or older, you need to include <script src="https://cdn.pubnub.com/pubnub-crypto.min.js"></script>

Using Access Manager, you can set admin mode for who has access to grant/deny a user permission, and general user mode.

First, after you enable Access Manager on your PubNub Admin Dashboard, you need to obtain your secretKey, along with your subscribe and publish keys.

Then create an instance with Admin Granting Capabilities by including the secretKey.

All the admin actions (setting secretKey, grant, revoke, etc.) should be done on the server-side, since you don't want to expose the secretKey on the client side.

The example below is in node.js:

var pubnub = new PubNub({
    subscribeKey: "mySubscribeKey",
    publishKey: "myPublishKey",
    secretKey: "secretKey"
});

Initialize your client with auth Key to identify each subscriber:

var randomAuthKey = PubNub.generateUUID();
   
var pubnub = new PubNub ({
	authKey: randomAuthKey, 
	subscribeKey: "mySubscribeKey",
	publishKey: "myPublishKey",
	secretKey: "secretKey"
});

You should generate an arbitrary auth Key. It can be an uuid, or an authentication token from OAuth, Facebook Connect or any other authentication service. Then the admin grants access to users using the auth Key.

To reset the auth Key, use setAuthKey() method:

pubnub.setAuthKey('my_new_auth_key')
 
You need to enable Access Manager from your Admin Dashboard.
 
Only admin can grant access to a user. This operation should be done on the server-side.

This example grants a user. who is associated with the particular auth Key, 60 minute read and write privilege on a particular channel:

pubnub.grant(
    {
        channelGroups: ['private-83659357'],
        authKeys: ['abcxyz123-auth-key'],
        ttl:60,
        read: true // false to disallow
        write: true, // false to disallow
    },
    function (status) {
        // handle status
    }
);

The default ttl value is 24 hours (1440 minutes).

If the auth Key param is not included, a global grant is issued to all users who are subscribed to the channel.

 
Only admin can revoke access to a user. This operation should be done on the server-side.
pubnub.grant(
    {
        channelGroups: ['private-83659357'],
        authKeys: ['abcxyz123-auth-key'],
        ttl:60,
        read: false, // false to disallow
        write: false, // false to disallow
        manage: false // false to disallow
    },
    function (status) {
        // handle status
    }
);

If the auth Key param is not included, access is revoked globally for all users who are subscribed to the channel.

PubNub client libraries offer built-in Advanced Encryption Standard (AES) 256-bit encryption. To use message encryption, set a cipherKey. Only a recipient who has the cipherKey is able to read the data, so the key needs to be hidden from any users, thus this is generally a good idea to keep it on server side.

Then publish/subscribe as usual.

var pubnub = new PubNub({
    subscribeKey: "mySubscribeKey",
    publishKey: "myPublishKey",
    cipherKey: "myCipherKey"
});

You can use the cipherKey on any device. The cipherKey is just a symmetric key used for encryption. What you need to ensure is that the cipherKey is transmitted securely from your server to device. You are not supposed to hard code your cipherKey. Messages exchanged over a TLS session are encrypted using a symmetric key too. During the SSL/TLS handshake, public key techniques are used to agree on the symmetric key. Also, cipher keys are only to be shared with trusted parties.

For more details, read: Message layer encryption with AES256

Enable Transport Layer Encryption with SSL/TLS by setting ssl param to true.

Then publish/subscribe as usual.

var pubnub = new PubNub({
	subscribeKey: "mySubscribeKey",
	publishKey: "myPublishKey",
	cipherKey: "myCipherKey",
	authKey: "myAuthKey",
	logVerbosity: true,
	uuid: "myUniqueUUID",
	ssl: true,
	presenceTimeout: 130
});

Malicious users may attempt to spoof the identity of users to impersonate other users. You may consider using a digital signature as a fingerprint.

To attach a digital signature to a message, first, create the signature by concatenation of the following base string (username + message). Then, calculate a message digest using SHA1 (signature_base_string) or SHA256 (signature_base_string).

Now you can use this cryptographically generated hash to sign with the user's private key using an ECC/RSA asymmetric algorithm.

Then attach it with the message object to publish:

{ 
    username: "Grumpy Cat",
    message: "Hello hoomans",
    signature: "tnnArxj06cWHq44gCs1OSKk/jLY" 
}

To receive the message with the signature, you will use the signature to verify the authenticity of the message. If the message was an attempted spoof, you will drop the message.

To verify the signature, recreate the signature by concatenation of the base string (username + message), then calculate a message digest using SHA1 or SHA256. Compare with the original message digest.

It is important to note that you should only print the username retrieved from the Public Key Trusted Source and not from the message content. The message content is only used for signing and verifying Identity. If an Identity is changed it must be relayed via the Public Key Infrastructure (PKI) using PubNub's Broadcast mechanism and Storage and Playback for Secure Authorization.

Access tokens, Identity Name/Photo and Asymmetric Keys must only be generated from trusted execution environments on your servers and relayed over a PubNub Authorized Read-only PKI data channel.

Digital signature is not a part of PubNub APIs, and the code isn't easy to explain here, so we decided not to discuss more and link to the full doc: User identification with digital signature message verification
 
You need to enable Push Notifications from your Admin Dashboard.

Push Notifications are used when the user closes the chat application or runs it in the background. On Apple devices the only solution is to use Apple Push Notification Service (APNs). Essentially PubNub will pass your messages to Apple and they will deliver it to the phone to alert users.

On Android you have 2 choices.

  1. Use Google Cloud Messaging (GCM)/Firebase Cloud Messaging (FCM) similar to APNs where PubNub passes the messages off to Google and they deliver the messages.
  2. Use a Background Thread this allows you to have the same low-latency message delivery. (See more on using Android background thread).

First, you must be registered to use Google Cloud Messaging (GCM)/Firebase Cloud Messaging (FCM) for Android, or Apple Push Notification Service (APNs) for iOS devices.

Each device that runs your app has a unique identifier called, either registration ID or device token, depending on its OS, and you need to register the token from your user's device to be able to send push notifications.

You need to code in either your native app, or use Cordova/PhoneGap with the Cordova Push plugin to obtain the token from your user.

For example, Android device ID size is up to 4k, and the string starts with APA, and APNS token is about 32 bytes in size.

Once you get the device-specific ID, save the token, then use PubNub mobile_gw_provision method and PNmessage object to set GCM/FCM or APNs message objects, to finally send a push message to the specific device.

var channel = 'room-1';
pubnub.push.addChannels(
	{
		channels: ['channel'],
		device: regid, // device-specific ID
		pushGateway: 'apns' // apns, gcm, mpns
	},
	function(status) {
		// handle status.error?
		var pushPayload = {
			"pn_apns": {
				"aps" : {
					"alert": "Yo! from GrumpyCat",
					"badge": 2,
					"sound": "melody"
				},
				"c" : "3"
			},
			"pn_gcm": {
				"data" : {
					"a" : "Yo! from GrumpyCat"
				}
			},
			"b" : "2"
		}
 
		pubnub.publish({ message: pushPayload }, function (status) {
			// handle publish status.
		})
    }
);
 
For more information, please read tutorials: Mobile push gateway tutorial

To display the time token in human-readable form, use JavaScript V4 Date object methods.

Using the subscribe success callback, the e returns an envelope that contains a 17-digit precision unix time (UTC), upon a publish success the callback returns the following:

p.subscribe({
  channel: channel,
  callback: function(m, e, c) {
    var t = e[1];
  }
});

To convert the timestamp to a UNIX timestamp, divide it by 10,000,000. For example, to obtain a locale date string in JavaScript V4,

var unixTime = t * 10000000;
var d = new Date(unixTime * 1000);
var localeDateTime = d.toLocaleString();

or directly from 17-digit precision time without converting to UNIX time,

var d = new Date(t/10000);
var localeDateTime = d.toLocaleString();

where localDateTime is something similar to 7/5/2015, 3:58:43 PM

User input from text fields may contain HTML tags, and especially <script> injections can be malicious. You should validate user inputs properly to avoid this.

The easiest way to prevent this is to simply remove <> to make the HTML into a string.

var text = input.value();
var safe_text = ('' + text).replace(/[<>]/g, '');

Alternatively, you may want to to escape the special characters, rather than just stripping off. See OWASP's XSS prevention cheat sheet for more information.

You may want to include user's physical locations as the user info.

Use HTML5 geolocation API to get the user's current location by using either WiFi, cell tower, GPS or AGPS from the browser:

if ("geolocation" in navigator) {
    // feature detection
    navigator.geolocation.getCurrentPosition(function(position) {
        var lat = position.coords.latitude;
        var lon = position.coords.longitude;
    });
}

For example, the location of PubNub office (in San Francisco) returns as lat = 37.7833866, and lon = -122.3995498.

To plot realtime locations, you can use PubNub open-source EON libraries to track location data from a PubNub stream and plot on Mapbox map.

Realtime Map with Multiple Markers

To get the city names, use the 3rd party APIs such as Geocoding API provided by Mapbox:

With Mapbox Reverse Goecoding API the requested feature data located at the input {lon},{lat} coordinates returns with a response includes at most one result from each geocoding dataset.

The example request:

https://api.mapbox.com/v4/geocode/mapbox.places/-122.3994321,37.783544.json?access_token=[your_mapbox_access_token]

and its response:

{"type":"FeatureCollection","query":[-122.3994321,37.783544],"features":[{"id":"address.275745210758781","type":"Feature","text":"3rd St","place_name":"300 3rd St, San Francisco, 94103, California, United States","relevance":1, ...

You may want to use geohash as a geographical unique identifier. Geohash is a latitude/longitude geocode system. It is a hierarchical spatial data structure which subdivides space into buckets of grid shape.

This particular geohash method works this way:

The precision increases as you increase the resolution. At resolution 0, each grid size is approximately 111km x 111km (= circumference of the earth 40,075km/360deg). Resolution 1 gives 11km x 11km grids, and resolution 2 gives 1.1km x 1.1km.

The example below is for SoMa district in San Francisco, California at lat: 37.7834462, lon: -122.3995248 (Geohash grid is 37-123).

SoMa district in San Francisco

Storing small data locally on a user's browser is easier, and can be a more suitable option than using an external database to store the data. For example, tracking if the user has logged in from a particular browser. You can do so with W3C Web Storage API using sessionStorage (non persistent) or localStorage (persistent).

To set data item:

localStorage.setItem('username', 'Nyan Cat');

To get the data:

var username = localStorage.getItem('username');

Web Storage is easier than storing data in cookies and does not share the data with the server.

There are many different design patterns, depending on your specific user interactions (e.g. click a message to read), to determine if a message has been read.

One general idea is to simply keep track of the PubNub timetoken to mark the last read messages, while the user is online.

See the section, Getting user online/offline status to see how to determine a user is online or not.

Timetoken can be retrieved at subscribe callback. (See the section Displaying the time a message is delivered)

Alternatively, you track the read messages by moving to another channel, such as channel-READ and check the IDs of the messages you want to mark as read or unread.

The publish API allows you to send messages up to 32kb, so if the file is small enough, encode a binary file in Base64 format and publish the data as usual.

To achieve this in JavaScript V4, first you need to convert an image to a canvas object then use toDataURL method.

var dataURL = canvas.toDataURL('image/jpeg');

Alternatively, use FileReader API to encode an image a base64, with file uploader, <input type="file"> with readAsDataURL method.

Or you can use the 3rd party modules to convert binary to base64, if you are coding in node.js.

When the file size is above 32kb, you need to provide your own storage servers.

You can store the binary files in a database such as Amazon S3, or other cloud storage services or on a CDN then send the URL over PubNub.

PubNub stores all messages per channel with the Storage & Playback feature add-on. Since it stores all messages in a time series, and doesn't expose Updating and Deleting, you need to implement this functionality.

There are two ways to do this:

  • Interleaving
  • Side-channel

Both cases, you send a message including deleted data with the default value of false.

{
    message_id : 10001,
    User : 'GrumpyCat',
    deleted : false
}

With the Interleaving pattern, you publish new versions of the same message to the same channel just like normal publishes. The subsequent messages must use the same message_id as the original message, and to simplify rendering, the messages for updates should contain the full content so that the original is not needed.

In the Side Channel pattern, you are publishing any updates and deletes to a separate channel that you also subscribe to. This means that you have a primary content channel and a side channel for updates/deletes.

With the Storage & Playback feature add-on, messages are stored on a per channel basis. This means that the side-channel has only update/delete messages in it, rather than both the original messages and the changes.