Build a secure chat web app with JavaScript, Auth0 and Pusher

build-secure-chat-web-app-javascript-auth0-pusher-users-header.png

Learn how to build a secure chat web application with Pusher, add user authentication with Auth0 Lock, and manage users from the Auth0 dashboard.

Introduction

In this tutorial, we will build a web application with a secure chat using JavaScript with Auth0 and Pusher.

Security is hard. Often when we build applications we want to allow only registered users to access the application. We want to be able to manage user accounts, see when they last logged in, be able to disable suspicious accounts and have a dashboard to view and manage all this data. We might also decide to support multi-factor authentication and social login.

But security isn’t just hard, it also takes a while to implement. What if there’s a service that could take away this part of the development hassle from you? Why spend weeks or months rolling your own auth? This is where Auth0 shines. In this tutorial, I’ll show you how to build a chat application with Pusher, add user authentication with Auth0 Lock, and manage users from the Auth0 dashboard.

Introduction to Auth0 and Pusher

Auth0 is an Authentication-as-a-Service (or Identity-as-a-Service) provider focused on encapsulating user authentication and management, which provides an SDK to allow developers to easily add authentication and manage users. Its user management dashboard allows for breach detection and multifactor authentication, and Passwordless login.

Pusher’s API’s and hosted infrastructure allow us to build scalable and reliable realtime applications. Pusher has a concept of channels and events which are fundamental to it. Channels provide a way of filtering data and controlling access to different streams of information, while events are the primary method of packaging messages in the Pusher system and form the basis of all communication.

Building the application

We will be building a chat application that’ll allow users to communicate with each other where everyone sees every other person’s messages. It’ll work similarly to how channels work in Slack: just one channel for everybody to communicate.

Here’s what we’ll be building:

Setting up the backend
We’ll start by building the backend which will facilitate receiving and broadcasting chat messages, serving static files, and also setting up Auth0 and Pusher.

First, you’ll need to signup for a Pusher and Auth0 account. Go to pusher.com and auth0.com and sign up for an account. To use Pusher API we have to signup and create a Pusher app from the dashboard. We can create as many applications as we want and each one will get an application id and secret key which we’ll use to initialise a Pusher instance on client or server side code.

Create a new Pusher account
To create a new Pusher app, click the Your apps side menu, then click the Create a new app button below the drawer. This brings up the setup wizard.

  • Enter a name for the application. In this case I’ll call it “chat”.
  • Select a cluster.
  • Select the option “Create app for multiple environments” if you want to have different instances for development, staging and production.
  • Select Vanilla JS as the frontend and NodeJS as the backend.
  • Complete the process by clicking Create App button to set up your app instance.

Since we’re building our backend in Node using Express, let’s initialise a new Node app and install the needed dependencies. Run the following command:

  • npm init and select the default options
  • npm i –save body-parser express pusher to install express and the Pusher node package

Add a new file called server.js which will contain logic to authenticate the Pusher client and also render the static files we’ll be adding later. This file will contain the content below:

1var express = require('express');
2    var bodyParser = require('body-parser');
3    var Pusher = require('pusher');
4
5    var app = express();
6    app.use(bodyParser.json());
7    app.use(bodyParser.urlencoded({ extended: false }));
8
9    var pusher = new Pusher({ appId: APP_ID, key: APP_KEY, secret:  APP_SECRET, cluster: eu });
10
11    app.post('/pusher/auth', function(req, res) {
12      var socketId = req.body.socket_id;
13      var channel = req.body.channel_name;
14      var auth = pusher.authenticate(socketId, channel);
15      res.send(auth);
16    });
17
18    app.post('/message', function(req, res) {
19      var message = req.body.message;
20      var name = req.body.name;
21      pusher.trigger( 'private-chat', 'message-added', { message, name });
22      res.sendStatus(200);
23    });
24
25    app.get('/',function(req,res){      
26         res.sendFile('/public/index.html', {root: __dirname });
27    });
28
29    app.use(express.static(__dirname + '/public'));
30
31    var port = process.env.PORT || 5000;
32    app.listen(port, function () {
33      console.log(`app listening on port ${port}!`)
34    });

We instantiate Pusher by passing in an object that contains the details of our app ID and secret key, which can be found on the App Keys tab in your Pusher dashboard. Pusher also provides a mechanism for authenticating users to a channel at the point of subscription. To do this, we expose an endpoint on the server that will validate the request and respond with a success or failure. This endpoint will be called by Pusher client libraries and can be named anything. We used the default name for this endpoint on Pusher, which is /pusher/auth. The line var auth = pusher.authenticate(socketId, channel); authenticates the client with Pusher and returns an authentication code to the calling client.

To allow this file to run when we start npm, we update package.json with the following value:

1"scripts": {
2        "start": "node server.js",
3        "test": "echo \"Error: no test specified\" && exit 1"
4      }

Create an Auth0 client
To create an Auth0 client

  • Select Clients from the side menu.
  • On the new page, click the Create Client button
  • Enter a name for the app and select Single Page App as an option
  • Click the Create button to create the client.

An Auth0 client provides us with Client Id and Secret which we’ll use to interact with Auth0 from the code. On the settings tab, we can see the Name, Client Id, Secret, Client Type and many more. I want to enable CORS for my domain http://localhost:5000, set the log out URL and the URL to redirect to after the user has been authenticated with Auth0. Update the following settings with http://localhost:5000

  • Allowed Callback URLs
  • Allowed Logout URLs
  • Allowed Origins (CORS)

Building the frontend
With the backend all good to go, we build the web page that will facilitate messaging. Create a folder named public which will contain the html and javascript file. Create two new files style.css and index.html with the following content:

1**style.css** 
2
3    @import url("http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css");
4    .chat
5    {
6        list-style: none;
7        margin: 0;
8        padding: 0;
9    }
10    .chat li
11    {
12        margin-bottom: 10px;
13        padding-bottom: 5px;
14        border-bottom: 1px dotted #B3A9A9;
15    }
16    .chat li.left .chat-body
17    {
18        margin-left: 60px;
19    }
20    .chat li.right .chat-body
21    {
22        margin-right: 60px;
23    }
24
25    .chat li .chat-body p
26    {
27        margin: 0;
28        color: #777777;
29    }
30    .panel .slidedown .glyphicon, .chat .glyphicon
31    {
32        margin-right: 5px;
33    }
34    .body-panel
35    {
36        overflow-y: scroll;
37        height: 250px;
38    }
39    ::-webkit-scrollbar-track
40    {
41        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
42        background-color: #F5F5F5;
43    }
44    ::-webkit-scrollbar
45    {
46        width: 12px;
47        background-color: #F5F5F5;
48    }
49    ::-webkit-scrollbar-thumb
50    {
51        -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3);
52        background-color: #555;
53    }
1**index.html** 
2
3    <!-- template from http://bootsnipp.com/snippets/6eWd -->
4    <!DOCTYPE html>
5    <html>
6    <head>
7        <!-- Latest compiled and minified CSS -->
8        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
9        <!-- Optional theme -->
10        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
11        <script
12            src="https://code.jquery.com/jquery-2.2.4.min.js"
13            integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44="
14            crossorigin="anonymous"></script>
15        <!-- Latest compiled and minified JavaScript -->
16        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
17        <link rel="stylesheet" href="style.css">
18        <script src="https://cdn.auth0.com/js/lock/10.18.0/lock.min.js"></script>
19        <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
20        <script src="index.js"></script>
21    </head>
22    <body>
23        <div class="container">
24        <div class="row form-group">
25            <div class="col-xs-12 col-md-offset-2 col-md-8 col-lg-8 col-lg-offset-2">
26                <div class="panel panel-primary">
27                    <div class="panel-heading">
28                        <span class="glyphicon glyphicon-comment"></span> <span id="username"></span>
29                        <div class="btn-group pull-right">
30                            <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">
31                                <span class="glyphicon glyphicon-chevron-down"></span>
32                            </button>
33                            <ul class="dropdown-menu slidedown">
34                                <li><a><span class="glyphicon glyphicon-refresh">
35                                </span>Refresh</a></li>
36                                <li><a><span class="glyphicon glyphicon-ok-sign">
37                                </span>Available</a></li>
38                                <li><a><span class="glyphicon glyphicon-remove">
39                                </span>Busy</a></li>
40                                <li><a><span class="glyphicon glyphicon-time"></span>
41                                    Away</a></li>
42                                <li class="divider"></li>
43                                <li><a id="logout"><span class="glyphicon glyphicon-off"></span>
44                                    Sign Out</a></li>
45                            </ul>
46                        </div>
47                    </div>
48                    <div class="panel-body body-panel">
49                        <ul class="chat">
50
51                        </ul>
52                    </div>
53                    <div class="panel-footer clearfix">
54                        <textarea id="message" class="form-control" rows="3"></textarea>
55                        <span class="col-lg-6 col-lg-offset-3 col-md-6 col-md-offset-3 col-xs-12" style="margin-top: 10px">
56                            <button class="btn btn-warning btn-lg btn-block" id="btn-chat">Send</button>
57                        </span>
58                    </div>
59                </div>
60            </div>
61        </div>
62    </div>
63    <script id="new-message" type="text/template">
64        <li id="{{id}}" class="right clearfix">
65            <div class="chat-body clearfix">
66                <div class="header">
67                    <small class="text-muted">{{name}}</small>
68                </div>
69                <p>
70                    {{body}}
71                </p>
72            </div>
73        </li>
74    </script>
75    </body>
76    </html>

This file uses template from bootsnip and also includes a script reference to Auth0 Lock <script src="https://cdn.auth0.com/js/lock/10.18.0/lock.min.js"></script>. Lock is a drop-in authentication widget that provides a standard set of behaviours required for and a customisable user interface. It provides a simple way to integrate with Auth0 with very minimal configuration.

We want to allow users to sign in when they enter the application and be able to send messages once they’re authenticated. Add a new file index.js with the following content:

1$(document).ready(function(){
2        // Initiating our Auth0Lock
3        let lock = new Auth0Lock(
4            'CLIENT_ID',
5            'CLIENT_DOMAIN',//example: lotus.auth0.com
6            {
7                auth: {
8                    params: {
9                        scope: 'openid profile'
10                    }   
11                },
12                autoclose: true,
13                closable: false,
14                rememberLastLogin: true
15            }
16        );
17
18        let profile = JSON.parse(localStorage.getItem('profile'));
19        let isAuthenticated = localStorage.getItem('isAuthenticated');
20
21        function updateValues(userProfile, authStatus) {
22            profile = userProfile;
23            isAuthenticated = authStatus;
24        }
25
26        if(!isAuthenticated && !window.location.hash){
27            lock.show();//show Lock widget
28        }
29
30        // Listening for the authenticated event
31        lock.on("authenticated", function(authResult) {
32            // Use the token in authResult to getUserInfo() and save it to localStorage
33            lock.getUserInfo(authResult.accessToken, function(error, profile) {
34                if (error) {
35                    // Handle error
36                    return;
37                }
38
39                localStorage.setItem('accessToken', authResult.accessToken);
40                localStorage.setItem('profile', JSON.stringify(profile));
41                localStorage.setItem('isAuthenticated', true);
42                updateValues(profile, true);
43                $("#username").html(profile.name);
44            });
45        });
46    });

We initialise Lock by passing it the Client Id of the app, your user domain which starts with your username followed by .auth0.com or .{YOUR_SELECTED_REGION}.auth0.com e.g lotus.eu.auth0.com. The widget is configurable and we can send in configuration options like closable, autoClose, and auth. Within the auth option we tell it to return the openid and profile claims.

We check if the user is authenticated and show the widget when they’re not. Once the user is authenticated, Lock emits the authenticated event which we’ve subscribed to. When it’s raised, we store the user profile and other credentials to localStorage and set the user’s name to be displayed on the page. Once the user is authenticated, we want to connect to Pusher and send messages across. Update index.js with the following code:

1if(!isAuthenticated && !window.location.hash){
2        lock.show();
3    }
4    else{
5
6        // Enable pusher logging - don't include this in production
7        Pusher.logToConsole = true;
8
9        var pusher = new Pusher('APP_SECRET', {
10            cluster: 'e.g eu',
11            encrypted: false
12        });
13
14        var channel = pusher.subscribe('private-chat');
15        channel.bind('message-added', onMessageAdded); 
16    }
17
18    function onMessageAdded(data) {
19        let template = $("#new-message").html();
20        template = template.replace("{{body}}", data.message);
21        template = template.replace("{{name}}", data.name);
22
23        $(".chat").append(template);
24    }

Pusher is initialised with the APP_SECRET and CLUSTER which you can get from the app dashboard on Pusher. We subscribe to a channel called private-chat . Pusher has 3 types of channels: Public, Private and Presence channel. Private and Presence channels let your server control access to the data you are broadcasting. Presence channels go further to force subscribers to register user information when subscribing. Private channels are named starting with private- and authenticated in the server when subscribing.

And finally we want to send the message to the user when they click send and also log them out when they select signout. Update index.js with the code below

1$('#btn-chat').click(function(){
2        const message = $("#message").val();
3        $("#message").val("");
4            //send message
5        $.post( "http://localhost:5000/message", { message, name: profile.name } );
6    }); 
7
8    $("#logout").click((e) => {
9        e.preventDefault();
10        logout();
11    });
12
13    function logout(){
14        localStorage.clear();
15        isAuthenticated = false;
16        lock.logout({ 
17            returnTo: "http://localhost:5000" 
18        });
19    }

When the user clicks the send button, we take the message and put it in an object with the user’s profile name and send it to the /message endpoint on the server. When the logout button is clicked, it calls the logout function which clears the data stored in localStorage and call lock.logout() which logs the user out on Auth0 and redirects them back to our website. With all these additions, index.js should have the following content:

1$(document).ready(function(){
2        // Initiating our Auth0Lock
3        let lock = new Auth0Lock(
4            'CLIENT_ID',
5            'CLIENT_DOMAIN',
6            {
7                auth: {
8                    params: {
9                        scope: 'openid profile'
10                    }   
11                },
12                autoclose: true,
13                closable: false,
14                rememberLastLogin: true
15            }
16        );
17
18        // Listening for the authenticated event
19        lock.on("authenticated", function(authResult) {
20            // Use the token in authResult to getUserInfo() and save it to localStorage
21            lock.getUserInfo(authResult.accessToken, function(error, profile) {
22                if (error) {
23                    // Handle error
24                    console.log(error);
25                    return;
26                }
27
28                localStorage.setItem('accessToken', authResult.accessToken);
29                localStorage.setItem('profile', JSON.stringify(profile));
30                localStorage.setItem('isAuthenticated', true);
31                updateAuthenticationValues(profile, true);
32                $("#username").html(profile.name);
33            });
34        });
35
36        let profile = JSON.parse(localStorage.getItem('profile'));
37        let isAuthenticated = localStorage.getItem('isAuthenticated');
38
39        function updateAuthenticationValues(userProfile, authStatus) {
40            profile = userProfile;
41            isAuthenticated = authStatus;
42        }
43
44        $("#logout").click((e) => {
45            e.preventDefault();
46            logout();
47        });
48
49        function logout(){
50            localStorage.clear();
51            isAuthenticated = false;
52            lock.logout({ 
53                returnTo: "http://localhost:5000" 
54            });
55        }
56
57        function onMessageAdded(data) {
58            let template = $("#new-message").html();
59            template = template.replace("{{body}}", data.message);
60            template = template.replace("{{name}}", data.name);
61
62            $(".chat").append(template);
63        }
64
65        if(!isAuthenticated && !window.location.hash){
66            lock.show();
67        }
68        else{
69            if(profile){
70                $("#username").html(profile.name);
71            }
72
73            // Enable pusher logging - don't include this in production
74            Pusher.logToConsole = true;
75
76            var pusher = new Pusher('APP_SECRET', {
77                cluster: 'eu',
78                encrypted: false
79            });
80
81            var channel = pusher.subscribe('private-chat');
82            channel.bind('message-added', onMessageAdded);
83
84            $('#btn-chat').click(function(){
85                const message = $("#message").val();
86                $("#message").val("");
87                 //send message
88                $.post( "http://localhost:5000/message", { message, name: profile.name } );
89            });  
90        }
91    });

To test the app, run npm start on the terminal and open http://localhost:5000 on two separate browsers. Here’s a run through of it:

Wrap

This is an app to show how you can use Pusher to send messages in real-time and secure the channels, add user authentication and account management with Auth0, and easily integrate to Auth0 using Auth0 Lock. On your auth0 dashboard you can see the total number of users, logins and new signups.

You can also see all your users when you click on the Users side menu. On this page you can see the list of your users and their mode of login.

Selecting a user takes you to a more detailed page where you can take various actions on the account, for example, blocking an account or sending a verification email.

Also on Pusher, you can go to your application dashboard, under the Stats, where you’ll see statistics concerning your application, such as connection frequency and how many messages were sent through that app. The combination of these two technologies makes it faster and easier to build real-time secured applications. You can find the code here on GitHub.