Professional Application Development in MEAN Stack - Part 10 - A
Date Published: 11/11/2018
In previous parts, we developed the Admin section to manage the Menu, User Messages, and Pages. In this part, we will secure the Admin section by implementing the login functionality. On the client side, we will learn how to implement the Angular Guard, JSON Web Token (JWT) and browser local storage. On the server side, we will use bcrypt to compare the hash password, JSON web token to create the public key and embed into the login API response.

Introduction

In previous parts, we developed the Admin section and it is open for everyone that is definitely not safe because everybody can mess up with the Menu, User and Page management, dah!. We need to secure the admin section and for that, we are going to implement the login functionality. For this application, we are assuming to have only one admin so we will keep the admin user and password in the config file (environment.json), but you are free to store them in the database for multiple users, we will also use the JSON Web Token(JWT) to keep the trusted relationship between client and server (as an anti-forgery token to avoid CSRF). The password would be stored as a hash string in the config file instead of plain string to add one more layer of security, we will use an online tool to hash the password. 

Let's Start

It is strongly recommended to read all previous parts before moving to this one, I can't promise you would understand all the steps I am going to list down without knowledge of previous parts.

  1. Please compile and run the Part 9 and make sure the application is working fine, if there is an issue, you can verify the steps or comment at the end, I would love to help to resolve any problem you are facing. 
  2. First, we will go to node.js server side and install two packages to create the Login API. 
    1. bcryptjs: The bcryptjs is password hashing package, this is a one-way hashing algorithm we will use to hash the input password and compare it with already stored hashed password in the config file. Right click on the server, select the option, Open in Terminal and run the command in TERMINAL tab: npm install bcryptjs --save
    2. jsonwebtoken: This package will take the already stored private key and create the public key called token. We will embed this token with Login API response to client and client would send it back to the server with all secured APIs request. The server will make sure the public key is the right one and the client is trusted to avoid CSRF. In the already opened server TERMINAL, run the command: npm install jsonwebtoken --save
  3. For JSON Web Token(JWT), we need the private key, you can add any random string for that but I like to generate it through the tool, you can visit https://passwordsgenerator.net/ to generate the complex key, I selected the 64 characters long with first 7 checkboxes checked. Go to the server -> config -> database.js and replace its content with the following code that contains the secret key, you can create a separate file as well to store the key:  
    module.exports = {
        database:'mongodb://127.0.0.1:27017/mazharncoweb',
        secret: '%yjhPbP*3gMdWTJapwfev2e7P6C9#mkvuABDL*qU=bb@@FZUkGZNnk36&VzC44&s'
      }
  4. Let's go to environment.json and check our admin username and password, you probably should already have it if you are following my article series. Edit the server -> environment.json and check you have USR and PW keys: 
    {
        "apps": [
            {
                "name": "mazharnco",
                "script": "./mazharnco.js",
                "watch": true,
                "env_development": {
                    "NODE_ENV": "development",
                    "ADMIN_EMAIL":"xxxxxxxxx@gmail.com",
                    "ADMIN_EMAIL_PW":"xxxxxxxxxxx",
                    "EMAIL_SERVICE":"Gmail",
                    "FROM_EMAIL":"xxxxxxxxxxxx@yahoo.com",
                    "PORT":"3000",
    
                    "USR":"admin@fullstackhub.io",
                    "PW":"$2y$12$PJ3jUL5txIPl1Yuz4kmYQucq/T4yK6SbV6K76YzyXl2aOcP6XqQR2"
                },
                "env_production": {
                    "NODE_ENV": "production",
                    "ADMIN_EMAIL":"XXXXXX@gmail.com",
                    "ADMIN_EMAIL_PW":"",
                    "EMAIL_SERVICE":"Gmail",
                    "FROM_EMAIL":"XXXXXXX@yahoo.com",
                    "PORT":"5001"
                }
            }
        ]
    }
  5. You can replace the USR with your email address and for PW key as I mentioned in the introduction we will save the hashed password so that user can't see and guess it. You can go to https://bcrypt-generator.com/ website and in the Encrypt section, enter your desired password and click the Hash! button. Copy the result from the top green alert in PW key's value. For now, I am keeping the password "fullstackhub", generating the hashcode for it and saving it in environment.json PW key's value.
  6. Next, we will create the User model class to hold the username and password. Right click on a server -> models and select the option New File, specify the name userMdl.js and add following code in it:  
    const mongoose = require('mongoose');
    const bcrypt = require('bcryptjs');
    
    const UserSchema = mongoose.Schema({
    
        EmailAddress: {
            type: String,
            required: true,
            unique: true
        },
        Password: {
            type: String,
            required: true,
            unique: true
        }
    });
    
    const User = module.exports = mongoose.model('user', UserSchema);
    
    module.exports.getUserByUsername = function (username, callback) {
        callback(null, process.env.USR === username);
    }
    
    module.exports.comparePassword = function (candidatePassword, hash, callback) {
        bcrypt.compare(candidatePassword, hash, (err, isMatch) => {
            if (err) throw err;
            callback(null, isMatch);
        });
    }
  7. On the top, we are creating the User schema that will create a users collection in the database but we will not be using it for now because username and password would be read from environment.json. We are just creating the collection for the future in case you decide to use it.
  8. We have two methods getUserByUsername() that is comparing the input username from client to already saved username as USR key in an environment.json file, the comparePassword() a function is taking the plain password (in our case "fullstackhub"), generating the hash string and comparing it with already saved password as PW key in environment.json
  9. Next, create the login API, right click on the server -> routes folder and select the option, New File, enter the file name user.js and hit the enter key. Add the following code in it: 
    const express = require('express');
    const router = express.Router();
    const User = require('../models/userMdl');
    const jwt = require('jsonwebtoken');
    const config = require('../config/database');
    
    router.post('/login', (req, res, next) => {
        const username = req.body.EmailAddress;
        const password = req.body.Password;
        User.getUserByUsername(username, (err, user) => {
            if (err) throw err;
            if (!user) {
                return res.json({ success: false, msg: "Invalid Email Address!" });
            }
    
            User.comparePassword(password, process.env.PW, (err, isMatch) => {
                if (err) return res.json({ success: false, msg: "Invalid Email Address/Password!" });
                if (isMatch) {
                    let data = {
                        EmailAddress: username,
                        Password: password
                    };
                    const token = jwt.sign({ data }, config.secret, {
                        expiresIn: 604800
                    });
                    res.json({
                        success: true,
                        msg: "Successfully logged-in as admin!",
                        token: token,
                        expiresIn: 604800
                    });
                } else {
                    return res.json({ success: false, msg: "Invalid Email Address/Password!" });
                }
            });
        });
    });
    
    module.exports = router;
    
  10. In the above file, we have only one login API that is receiving the username and password from the client:
    1. First, we are calling the getUserByUsername() function from userMdl.js, verifying that username is same as in an enviornment.json file, if not, immediately returning the error response Invalid Email Address!.
    2. If the username does match, the control is calling the comparePassword() function from userMdl.js to verify the password against enviroment.json passsword. Just a reminder, the user will enter (in our case, "fullstackhub") that will be hashed and compared through bcrypt.compare() method
    3. If both username and password do match, we are creating the data object having username and password. This data object along secret key defined in database.js are passed as arguments to jwt.sign() method to create the public key (token). This token is returning back with a successful response where it would be stored in localStorage on the client side. 
  11. The last step on the server side is to add the login API route in a mazharnco.js file. Right click on the server -> mazharnco.js file and replace its content with the following:  
    const express = require('express');
    const path = require('path');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const contacts = require('./routes/contact');
    const menus = require('./routes/menu');
    const pages = require('./routes/page');
    const users = require('./routes/user');
    const mongoose = require('mongoose');
    const config = require('./config/database');
    
    mongoose.connect(config.database, function (err) {
        if (err) {
            console.log('Not connected to the database: ' + err);
        } else {
            console.log('Successfully connected to MongoDB');
        }
    });
    
    var app = express();
    
    app.engine('html', require('ejs').renderFile);
    app.use(express.static(path.join(__dirname, './views')));
    app.use('/uploads', express.static(path.join(__dirname, '/uploads')));
    
    app.use(bodyParser.urlencoded({ extended: false }));
    app.use(bodyParser.json());
    app.use(cors());
    
    app.use("/api", contacts);
    app.use("/api", menus);
    app.use("/api",pages);
    app.use("/api",users);
    
    app.get('*', function (req, res) {
        res.render(path.join(__dirname, './views/index.html')); // load our public/index.html file
    });
    
    const port =  process.env.PORT;
    
    app.listen(port, function () {
        console.log('Server started on port ' + port);
    });
  12. Run the server application by pm2 start environment.json --env development command, go to Postman and make the following API call, you should get successful response and token:    https://fullstackhub.io/uploads/a3db06701ace779983ce96e6790fadf11541984923801.gif
  13. Change the password and username and try different combinations. In Part B, we will develop a client application to secure the admin section. 


Keywords: angular reactive form tutorial, Node.js nodeemailer, MEAN stack tutorial, Angular 5 tutorial for beginners, MEAN Stack tutorial for beginners, rxjs tutorial or beginner, rxjs vs Promise,nodemailer,pm2, node.js MongoDB, node.js mongoose configuration, angular data grid control, free angular data grid control, tinymce angular, angular text editor, angular dynamic page, multer, mime-types, save files on storage in node.js, bcrypt, json web token