Build

AngularJS Chat Tutorial: OAuth 2.0 User Authentication

13 min read Michael Carroll on May 8, 2018

This tutorial walks through building chat with our core pub/sub technology (and other PubNub features). We recently launched ChatEngine, a new framework for rapid chat development.

Check out the ChatEngine AngularJS tutorial. →

Welcome back to Part 5 of our PubNub series on how to build a complete chat app with PubNub’s AngularJS SDK.

In Part 4: Displaying a Typing Indicator in Your AngularJS Chat App Using Custom State with Presence API, you learned how to add a real-time typing indicator that shows who is currently typing, using custom state attribute with the Presence API.

In this tutorial, we will learn how to use OAuth 2.0 to authenticate users in your app and Access manager API to secure the communications through the PubNub channels.

This tutorial will walk through three topics:

  1. Implementing the Github OAuth2 authentication
  2. Securing access to channels using Access Manager API and the OAuth token
  3. Implementing the logout feature that is revoking access to the PubNub channels

Here’s how our chat app will look at the end of this tutorial:

Chat app with user authentication screenshot
Open the demo in the browser Demo:
The demo of the AngularJS chat app is available here
Get the AngularJS chat app demo source code on GitHub Source code:
The source code is available in the github repository

 

Authentication Design Pattern

In the app, we will use the Github OAuth provider to log in users. Once they are logged in, we will grant access to the PubNub channels. OAuth2 authorization flow is not trivial, but it’s a really convenient way to manage the authorizations in your apps.

Below is a schema of the flow we are going to implement in the application:

Flow chat app with user authentication

1. The AngularJS app is requesting to GitHub the authorization code by opening the GitHub popup window.
2. Github is issuing an authorization code.
3.-7. The AngularJS app is requesting an Access token to a Node.js Server which is requesting it to the GitHub API, granting access to the PubNub channels using the OAuth token and finally sending it back to the AngularJS app.
8. The AngularJS is using the OAuth token with PubNub in order to be allowed to publish or subscribe to the channels.

As we are going to implement a Node.js server, we need to adjust the directory structure :

→ Isolate the AngularJS app in a client directory.
→ Introduce a server directory

You should get the following directory structure:

angularjs-hubbub/
├── client
├── server

Authenticate the Users with OAuth2

First, we will see how to implement OAuth2 in order to allow the users to login with GitHub.
I picked GitHub as the authentication layer, but you can use the Google or Twitter OAuth provider, your Enterprise LDAP solution, your JWT tokens, or your own custom solution.

What is OAuth2 ?

Unfortunately, it would take a number of blog posts to properly explain OAuth2, but here are the fundamental concepts:

The User (you as a Github user) is requesting an authorization through the Client (the AngularJS app) to the Authorization server (Github) and can communicate with the Resource Server (the Node.js server and the Github API) with the access token that has been issued.

It’s more complicated than just providing a login / password to a Node.js server, but it allow multiple things such as controlling which client is authorized to login, set an authorization lifetime, etc…

Below is the abstract protocol flow:

Oauth2 Abstract flow

And this is how we implement OAuth in our application:

OAuth2 flow in PubNub AngularJS application

It seems complicated but don’t worry, we will be using libraries that will help us a lot.

Obtaining Your GitHub OAuth2 Keys

In order to implement the authentication with GitHub feature, you will first need to create a GitHub OAuth app:

Creating a GitHub Oauth2 application
    1. Visit https://github.com/settings/profile
    2. Select OAuth applications in the left panel
    3. Go to Developer applications tab, then click on the Register new application button
      1. Application name: Your app name
      2. Homepage URL: http://localhost:3000
      3. Authorization callback URL: http://localhost:3000
      4. Click on the Register application button
      5. Get your Client ID and Client Secret. We will need then in later steps

Configuring the OAuth2 Library

Now that we have created the Github OAuth app, we need to implement the OAuth authentication flow. We will be using the AngularJS Satellizer library that will take care of most of the OAuth2 authentication flow for us.

OAuth2 authentication flow

In this diagram, all the arrows in red are describing the steps that are taken care of by the Satellizer library. The only thing we will need is to configure Satellizer the right way and implement a step (described by the blue arrow) in our Node.js server.

→ Install the Satellizer library in your app:

bower install satellizer --save

→ Inject the Satellizer service as a dependency in app.js

→ In app.config.js, configure Satellizer with your Github client ID, the same redirectUri you specified in the Github OAuth app and finally the URL of the server that will be requested to authenticate the user.

.config(['$authProvider', function($authProvider) {
    $authProvider.github({
      clientId: "GITHUB_CLIENT_ID............",
      redirectUri: "http://localhost:9000/",
      url: "http://localhost:3000/auth/github",
    });
  
    $authProvider.httpInterceptor = true;
}]);

Creating the Login View

Login view of the AngularJS chat app

Create a “Sign in with Github button” component that we will put in later in the login view.

→ In login/sign-in-with-github-button.html, create a button, that upon clicking, calls an authenticate function of the component:

<button ng-click="authenticate()" >Sign in with github</button>

→ In login/sign-in-with-github-button.directive.js, create the authenticate function that calls the $auth.authenticate(‘github’) function of satellizer and redirects to the home page once logged in.

angular.module('app').directive('signInWithGithubButton', function() {
    return {
        restrict: "E",
        templateUrl: 'components/login/sign-in-with-github-button.html',
        controller: function($scope, $auth, $location, ngNotify) {
            $scope.authenticate = function() {
                $auth.authenticate('github')
                    .then(function(response) {
                        $location.path('/');
                    })
                    .catch(function(response) {
                        ngNotify.set('Authentication failed.', {
                            type: 'error',
                            sticky: true,
                            button: true,
                        });
                    });
            };
        }
    };
});

→ In the app/views folder, create a file called login.html and include the sign-in-with-github-button component.

<div class="login-container">
      <sign-in-with-github-button></sign-in-with-github-button>
</div>

→ Then, in app.routes.js add a route entry that points to the login view:

.when('/login', {
          templateUrl: 'views/login.html'
})

Try this by yourself: go to the login page and click the login button, then authenticate with GitHub.

OAuth2 login failure

Unfortunately, this will not work yet because Satellizer tries to reach our server at http://localhost:3000/auth/github but there is no server running yet.

So, we are going to implement the server in the next steps.

OAuth2 authentication flow

If you look at the diagram above, we will implement the missing part that is represented by the blue arrows. The server will grab the authorization code from the request made by Satellizer in the AngularJS app, ask github for the access token, then register the user in the database with its access token and send back the access token to the AngularJS app.

Implementing the NodeJS Authentication Server

First, go to the server directory and install node and express

npm install node --save
npm init
npm install express --save

You can use the express generator to create the base of your Express server:

npm install express-generator -g
express .

It generates a number of files and the app.js file will be the endpoint of our server.

Before getting started, we will need to set up two external libraries that will be useful in our app:

  • The first one is octonode that is a github API wrapper for NodeJS
  • The second one is NeDB, which is a simple database for Node.JS that doesn’t require any dependency or server. It runs on its own.
npm install octonode --save
npm install nedb --save

Then, you need to require the libraries in app.js

→ In app.js, setup NeDB by creating a users datastore that save the user in db/users.db

db = {};
db.users = new Datastore({ filename: 'db/users.db', autoload: true });

→ In app.js, create the endpoint POST /auth/github

app.post('/auth/github', function(req, res) {
  res.status(200).send();
});

For now, this endpoint doesn’t do much more than send back an HTTP status code 200 to the AngularJS app.

→ Finish implementing the endpoint so that it does the following:

  • gets the authorization_code from the request that has been sent by the AngularJs app
  • exchanges the authorization_code for the access token by calling the github API: https://github.com/login/oauth/access_token
  • calls the github API with the access token to get the user profile information (use the octonode wrapper to help you)
  • saves the user in the database with its access token if he doesn’t exist already.
  • sends the access token to the client

You should have something similar to the following below:

app.post('/auth/github', function(req, res) {
    var accessTokenUrl = 'https://github.com/login/oauth/access_token';
    var params = {
        code: req.body.code,
        client_id: process.env.GITHUB_CLIENT_ID,
        client_secret: process.env.GITHUB_CLIENT_SECRET,
        redirect_uri: req.body.redirectUri
    };
    // Exchange authorization code for access token.
    request.post({
        url: accessTokenUrl,
        qs: params
    }, function(err, response, token) {
        var access_token = qs.parse(token).access_token;
        var github_client = github.client(access_token);
        // Retrieve profile information about the current user.
        github_client.me().info(function(err, profile) {
            if (err) {
                return res.status(400).send({
                    message: 'User not found'
                });
            }
            var github_id = profile['id'];
            db.users.find({
                _id: github_id
            }, function(err, docs) {
                // The user doesn't have an account already
                if (_.isEmpty(docs)) {
                    // Create the user
                    var user = {
                        _id: github_id,
                        oauth_token: access_token
                    }
                    db.users.insert(user);
                }
                // Update the OAuth2 token
                else {
                    db.users.update({
                        _id: github_id
                    }, {
                        $set: {
                            oauth_token: access_token
                        }
                    })
                }
            });
        });
        res.send({
            token: access_token
        });
    });
});

→ Run the server and try to login again. It’s working and you will be redirected to the chat view.

There are still two things that you need to implement:

  • Protecting the chat app so that it redirects to the login page if not authenticated
  • Setting the current user to the GitHub user – instead of anonymous robots

Under the hood, satellizer is saving the OAuth token sent back from the server that we will be able to use later. Just call $auth.getToken() to get the token.

Getting the Authenticated GitHub User

Now, we want to switch the anonymous users to the GitHub users currently authenticated.

→ Update the services/current_user.service.js to fetch the GitHub user id from the oauth_token. Instead of returning a random id, the fetch method should return a promise that will be resolved when the user has been fetched.

angular.module('app')
    .factory('currentUser', ['$http', '$auth', function currentUserFactory($http, $auth) {
        var userApiUrl = 'https://api.github.com/user';
        var token = $auth.getToken()
        var authenticatedUser = null
        var fetch = function() {
            return $http({
                    cache: true,
                    method: 'GET',
                    url: userApiUrl
                })
                .then(function(user) {
                    authenticatedUser = user.data;
                    return user.data
                })
        };
        var get = function() {
            return authenticatedUser;
        };
        return {
            fetch: fetch,
            get: get
        }
    }]);

→ In services/message-factory.js, update the message-factory to send the github ID and login

var sendMessage = function(messageContent) {
      // Don't send an empty message 
      if (_.isEmpty(messageContent))
          return;
      Pubnub.publish({
          channel: self.channel,
          message: {
              uuid: (Date.now() + currentUser.get().id.toString()),
              content: messageContent,
              sender: { 
                        uuid: currentUser.get().id.toString(),
                        login: currentUser.get().login
                      },
              date: Date.now()
          },
      });
  };

→ Update the message_factory.js, online_users.service.js and typing-indicator.service.js to use the currentUser.get().id method instead of old currentUser method.
→ Update the message-list, message-item components to display the user GitHub login.
→ Update the user avatar directive in shared/user-avatar.directive.js to display the GitHub avatar of the user.

angular.module('app').directive('userAvatar', function() {
    return {
        restrict: "E",
        template: '<img ng-src="{{avatarUrl}}" alt="{{uuid}}" class="circle">',
        scope: {
            uuid: "@",
        },
        replace: true,
        controller: function($scope) {
            // Generating a uniq avatar for the given uniq string provided using robohash.org service
            $scope.avatarUrl = '//avatars.githubusercontent.com/u/' + $scope.uuid;
        }
    };
});

The Authentication Service

→ Create an authentication service in services/authentication.service.js

→ Create a login function that fetches the current user from the CurrentUserService and set the PubNub uuid with the GitHub ID. Then, start to subscribe to the PubNub message channel.

angular.module('app')
.factory('AuthenticationService', ['Pubnub', '$auth', 'currentUser', '$http',
    function AuthenticationService(Pubnub, $auth, currentUser, $http, config) {
        var channel = "messages";
        var login = function() {
            return currentUser.fetch().then(function() {
                Pubnub.set_uuid(currentUser.get().id)
                Pubnub.subscribe({
                    channel: channel,
                    noheresync: true,
                    triggerEvents: true
                });
            });
        };
        return {
            login: login
        };
    }
]);

Requiring the Authentication

→ In app.routes.js, create a requireAuthentication method that redirects users to the login page if they are not authenticated and login these users through the AuthenticationSevice#login method if they are authenticated.

// Redirect to the login page if not authenticated
var requireAuthentication = function($location, $auth, AuthenticationService) {
    if ($auth.isAuthenticated()) {
        return AuthenticationService.login()
    } else {
        return $location.path('/login');
    }
};

→ Call this method in the resolve statement of the main route.

.when('/', {
    templateUrl: 'views/chat.html',
    resolve: {
        requireAuthentication: requireAuthentication
    }
})

Securing Access to PubNub Channels with Access Manager API

The Access Manager API

One of the fundamentals of building a successful chat app application is having a great security model. Unfortunately, it also happens to be one of the most difficult and often overlooked features when building a chat app. Access Manager allows you, through a simple API to grant, to audit and revoke user permissions on PubNub channels.

You will need to first activate the Access Manager add-on in your app. Go to the Admin Dashboard. Select the app you are working on and go to Application add-ons to enable Access Manager.

When Access Manager is enabled, all of the channels for your app will be locked and you will need to grant user permissions to channels. We will walk through all of the details in the next steps.

If you want to learn more about Access Manager, watch the video below from the University of PubNub

On the Server Side

→ Install the PubNub javascript SDK

cd server
npm install pubnub --save

→ Require and init PubNub with your keys:

var pubnub = require('pubnub');
pubnub = pubnub.init({
    Subscribe_key: 'PUBNUB_SUBSCRIBE_KEY',
    Publish_key: 'PUBNUB_PUBLISH_KEY',
    Secret_key: 'PUBNUB_SECRET_KEY',
    auth_key: 'NodeJS-Server',
    ssl: true
})

→ In server/app.js, create a grantAccess function that grant access to the message channel to an OAauth token.

You will need to grant access to the messages channel as well as the presence channel called messages-pnpres

var grantAccess = function(oauth_token, error, success) {
  pubnub.grant({
    channel: ['messages', 'messages-pnpres'],
    auth_key: oauth_token,
    read: true,
    write: true,
    ttl: 0,
    callback: success,
    error: error
  });
};

→ Improve the /auth/github endpoint function in order to grant access to the PubNub channels prior to sending the OAuth token back to the client:

request.post({ url: accessTokenUrl, qs: params }, function(err, response, token) {   
   // ... Some code omitted here
   // ...
   var error = function(){ res.status(500).send(); } 
   var success = function(){ res.send({token: access_token}); }
   grantAccess(access_token, error, success);
});

On the Client Side

Now that the access_token has been granted to be used to publish and subscribe to the PubNub channels, we need to use it in our AngularJS app if we don’t want to get a forbidden error when subscribing and publishing:

→ In services/authentication.service.js, set the PubNub auth_key with the OAuth token

var login = function() {
    return currentUser.fetch().then(function() {
        // ….
        Pubnub.auth($auth.getToken())
        //…..
    });
};

Logout Feature and Revoking Access to PubNub Channels

In this part, we will learn how to implement the logout feature that revokes a user’s access to the channels.

On the Client Side

→ In services/authentication.service.js, create a logout function that will both logout from the client and the server:

var serverSignout = function() {
    var url = config.SERVER_URL + 'logout'
    return $http({
        method: 'POST',
        url: url
    })
};
var clientSignout = function() {
    $auth.logout()
    Pubnub.unsubscribe({
        channel: channel
    });
    $cacheFactory.get('$http').removeAll();
};
var logout = function() {
    return serverSignout().then(function() {
        clientSignout();
    });
};

→ In app.routes.js, create a logout route that calls the AuthenticationService#logout function:

when('/logout', {
    template: null,
    controller: function(AuthenticationService, $location, ngNotify) {
        AuthenticationService.logout().catch(function(error) {
            // The logging out process failed on the server side
            if (error.status == 500) {
                ngNotify.set('Logout failed.', {
                    type: 'error'
                });
            }
        }).finally(function() {
            $location.path('/login');
        });
    }
})

→ Create a link somewhere in your app to point to the logout route.

On the Server Side

→ In app.js, create an ensureAuthenticated middleware that will ensure for certain route that a user is authenticated before reaching it.

This middleware will ensure the user associated with the OAuth token exists in order to process the request. If it is not the case it will immediately send an http status code 401 back.

function ensureAuthenticated(req, res, next) {
     if (!req.header('Authorization')) {
         return res.status(401).send({
             message: 'Please make sure your request has an Authorization header'
         });
     }
     var token = req.header('Authorization').split(' ')[1];
     // Check if the OAuth2 token has been previously authorized
     db.users.find({
         oauth_token: token
     }, function(err, users) {
         // Unauthorized
         if (_.isEmpty(users)) {
             return res.status(401).send({
                 message: 'Unauthorized'
             });
         }
         // Authorized
         else {
             req.token = token;
             req.user_id = users[0].user_id
             next();
         }
     });
 }

→ Implement the revokeAccess method

var revokeAccess = function(oauth_token, error, success){
      pubnub.revoke({ 
        channel: ['messages', 'messages-pnpres'], 
        auth_key: oauth_token, 
        callback: success,
        error: error
      });
  };

→ Implement the logout endpoint and ensure the request goes through the ensureAuthenticated middleware before reaching the logout endpoint.

app.post('/logout', ensureAuthenticated, function(req, res) {
    
    var error = function(){ res.status(500).send(); } 
    var success = function(){ 
      db.users.update({ oauth_token: req.token }, { $set: { oauth_token: null } } )
      res.status(200).send(); 
    }
    revokeAccess(req.token, error, success)
  });

→ Try to logout and login.

That’s it! I hope you’ve enjoyed reading this tutorial.

If you have any questions or feedback, don’t hesitate to shoot me an email: martin@pubnub.com

Subscribe to our newsletter and we’ll keep you posted about the next AngularJS tutorials.

In the next tutorial, we are going to learn how to use the channel groups to create a friends list that shows the online status of your friends, but not all of the users in the chat room.

angularjs-chat-app-with-friend-list

See you in Part 6 !

0