https://www.learn2crack.com/2016/09/android-user-registration-login-node-server.html


In this series of tutorials we will develop a complete login, registration and authentication system with Node.js and MongoDBdatabase. We already have a old tutorial published two years back. This will be a updated one with latest code.

Some changes and features when compared to the old one,

-> Use JSON Web Token for session handling.

-> Passwords are hashed using Bcrypt hashing algorithm.

-> Basic Authentication for login authentication.

-> Use ES6 syntax and Promises instead of callbacks.

The ES6 Features we use are,

-> Promises

-> Arrow functions

-> const and let identifiers instead of var

Note :

ES6 is the latest revision of JavaScript which was released in 2015.

Prerequisites

-> Setup Node.js run time and npm in your Operating System (Windows, Linux or OS X). Download Node.js from the link given below.

https://nodejs.org

-> Download and install MongoDB server in your Operating System (Windows, Linux or OS X).

https://www.mongodb.com/download-center

-> Download and install Robomongo, a GUI client used to create and view databases.

https://robomongo.org/

Creating MongoDB Database

Using Robomongo create a new MongoDB database named node-login.

Then right click on the created database and select Open Shell.

Type the following code in the shell,

db.getCollection('users').createIndex( { "email": 1 }, { unique: true } )

The above code makes the email field as unique, similar to primary key in Relational database such as MySQL.

Download Complete Project

You can download the complete project as zip or fork from our Github repository.

Creating Project

Create a new directory named node-login and switch to that directory. Create a new package.json file to define all the required Node dependencies. The following dependencies are required.

package.json

{
  "name": "node-login",
  "version": "1.0.0",
  "main": "app.js",
  "dependencies": {
    "basic-auth": "^1.0.4",
    "bcryptjs": "^2.3.0",
    "body-parser": "^1.15.2",
    "express": "^4.14.0",
    "jsonwebtoken": "^7.1.9",
    "mongoose": "^4.6.0",
    "morgan": "^1.7.0",
    "nodemailer": "^2.6.0",
    "randomstring": "^1.1.5"
  }
}

Lets look at what these modules does,

basic-auth

This is used for login authentication. The email and password is sent by client as a Base64 encoded string in header which is decoded by this module.

bcyptjs

Use to create and verify password hash using bcrpyt hasing algorithm.

body-parser

This is used to parse the JSON body in each request.

express

This is the complete framework used to create RESTful web services.

jsonwebtoken

This is used to create JSON Web Token which is used for session handling.

mongoose

This module is used to connect to MongoDB database.

morgan

This module is used to print logs in terminal for each http request.

nodemailer

This module is used to sent mail using SMTP for forgot password process.

randomstring

This module is used to generate random string which we use as a token for reset password.

Project Structure

The image given below shows the exact project structure.

sublime

Creating config.json

Create a new config.json file. This file will be present in config directory. This file has some predefined constants such as email id, password and secret for creating JSON Web Token. The email id and password defined in this file will be used to send mail using SMTP. I have tested with Gmail.

config.json

{ 
 "name" : "Your Name",
 "email" : "YourEmail@gmail.com",
 "password" : "Your Email Password",
 "secret" : "Learn2Crack"
}

Creating MongoDB model

Create a user.js file in which we define our MongoDB collection schema. Instead of using var, we are going to use ES6 constand let identifier for assigning variables. The const is immutable where let is mutable also both are block scoped when compared to var.

user.js

'use strict';

const mongoose = require('mongoose');

const Schema = mongoose.Schema;

const userSchema = mongoose.Schema({ 

	name 			: String,
	email			: String, 
	hashed_password	: String,
	created_at		: String,
	temp_password	: String,
	temp_password_time: String

});

mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost:27017/node-login');

module.exports = mongoose.model('user', userSchema);

Creating functions

We have 4 functions files register.jslogin.jsprofile.js and password.js which has functions defined for the subsequent operation.

The register.js file consists of register function which saves data to MongoDB database. Instead of using the default Javascript callbacks we are using the new ES6 Promises. When creating a Promise we call resolve() for success operation and reject() for failure operation. Similarly when calling a promise then() is called for success and catch() is called for failure. We also replaced function definition with ES6 Arrow functions.

The login function is defined in login.js which authenticates the user and returns a JSON Web Token or else it throws a error.

Similarly profile.js has fucnction which returns users profile.

Finally the password.js file has definition for change password and rest password functionalities. The rest password is a two step process. The first step is to generate a random token and send to user’s email using nodemailer. The second step is to verify the token and change the password to a new one.

register.js

'use strict';

const user = require('../models/user');
const bcrypt = require('bcryptjs');

exports.registerUser = (name, email, password) => 

	new Promise((resolve,reject) => {

	    const salt = bcrypt.genSaltSync(10);
		const hash = bcrypt.hashSync(password, salt);

		const newUser = new user({

			name: name,
			email: email,
			hashed_password: hash,
			created_at: new Date()
		});

		newUser.save()

		.then(() => resolve({ status: 201, message: 'User Registered Sucessfully !' }))

		.catch(err => {

			if (err.code == 11000) {

				reject({ status: 409, message: 'User Already Registered !' });

			} else {

				reject({ status: 500, message: 'Internal Server Error !' });
			}
		});
	});

login.js

'use strict';

const user = require('../models/user');
const bcrypt = require('bcryptjs');

exports.loginUser = (email, password) => 

	new Promise((resolve,reject) => {

		user.find({email: email})

		.then(users => {

			if (users.length == 0) {

				reject({ status: 404, message: 'User Not Found !' });

			} else {

				return users[0];

			}
		})

		.then(user => {

			const hashed_password = user.hashed_password;

			if (bcrypt.compareSync(password, hashed_password)) {

				resolve({ status: 200, message: email });

			} else {

				reject({ status: 401, message: 'Invalid Credentials !' });
			}
		})

		.catch(err => reject({ status: 500, message: 'Internal Server Error !' }));

	});

profile.js

'use strict';

const user = require('../models/user');

exports.getProfile = email => 

	new Promise((resolve,reject) => {

		user.find({ email: email }, { name: 1, email: 1, created_at: 1, _id: 0 })

		.then(users => resolve(users[0]))

		.catch(err => reject({ status: 500, message: 'Internal Server Error !' }))

	});

password.js

'use strict';

const user = require('../models/user');
const bcrypt = require('bcryptjs');
const nodemailer = require('nodemailer');
const randomstring = require("randomstring");
const config = require('../config/config.json');

exports.changePassword = (email, password, newPassword) => 

	new Promise((resolve, reject) => {

		user.find({ email: email })

		.then(users => {

			let user = users[0];
			const hashed_password = user.hashed_password;

			if (bcrypt.compareSync(password, hashed_password)) {

				const salt = bcrypt.genSaltSync(10);
				const hash = bcrypt.hashSync(newPassword, salt);

				user.hashed_password = hash;

				return user.save();

			} else {

				reject({ status: 401, message: 'Invalid Old Password !' });
			}
		})

		.then(user => resolve({ status: 200, message: 'Password Updated Sucessfully !' }))

		.catch(err => reject({ status: 500, message: 'Internal Server Error !' }));

	});

exports.resetPasswordInit = email =>

	new Promise((resolve, reject) => {

		const random = randomstring.generate(8);

		user.find({ email: email })

		.then(users => {

			if (users.length == 0) {

				reject({ status: 404, message: 'User Not Found !' });

			} else {

				let user = users[0];

				const salt = bcrypt.genSaltSync(10);
				const hash = bcrypt.hashSync(random, salt);

				user.temp_password = hash;
				user.temp_password_time = new Date();

				return user.save();
			}
		})

		.then(user => {

			const transporter = nodemailer.createTransport(`smtps://${config.email}:${config.password}@smtp.gmail.com`);

			const mailOptions = {

    			from: `"${config.name}" <${config.email}>`,
    			to: email,  
    			subject: 'Reset Password Request ', 
    			html: `Hello ${user.name},

    			     Your reset password token is <b>${random}</b>. 
    			If you are viewing this mail from a Android Device click this <a href="http://learn2crack/${random}">link</a>. 
    			The token is valid for only 2 minutes.

    			Thanks,
    			Learn2Crack.`

			};

			return transporter.sendMail(mailOptions);

		})

		.then(info => {

			console.log(info);
			resolve({ status: 200, message: 'Check mail for instructions' })
		})

		.catch(err => {

			console.log(err);
			reject({ status: 500, message: 'Internal Server Error !' });

		});
	});

exports.resetPasswordFinish = (email, token, password) => 

	new Promise((resolve, reject) => {

		user.find({ email: email })

		.then(users => {

			let user = users[0];

			const diff = new Date() - new Date(user.temp_password_time); 
			const seconds = Math.floor(diff / 1000);
			console.log(`Seconds : ${seconds}`);

			if (seconds < 120) { return user; } else { reject({ status: 401, message: 'Time Out ! Try again' }); } }) .then(user => {

			if (bcrypt.compareSync(token, user.temp_password)) {

				const salt = bcrypt.genSaltSync(10);
				const hash = bcrypt.hashSync(password, salt);
				user.hashed_password = hash;
				user.temp_password = undefined;
				user.temp_password_time = undefined;

				return user.save();

			} else {

				reject({ status: 401, message: 'Invalid Token !' });
			}
		})

		.then(user => resolve({ status: 200, message: 'Password Changed Successfully !' }))

		.catch(err => reject({ status: 500, message: 'Internal Server Error !' }));

	});

Defining routes

All the RESTful endpoints are defined in routes.js file using the new Express 4 router. The default root endpoint returns a welcome message.

The /authenticate endpoint is used for login operation. The POST request in /users endpoint is used to add a new user.

The GET request in /users/email endpoint is used to get the user profile details. The generated JSON Web Token should be present in the header for the field x-access-token.

Similarly PUT operation is used to change the password using old and new password. Also x-access-token should be present in header for this request.

Finally POST request in /users/email/password is used for reset password operation. Sending a empty body will trigger the operation which sends a token in email. This token expires in 2 minutes. Sending body with token and new password resets the password.

routes.js

'use strict';

const auth = require('basic-auth');
const jwt = require('jsonwebtoken');

const register = require('./functions/register');
const login = require('./functions/login');
const profile = require('./functions/profile');
const password = require('./functions/password');
const config = require('./config/config.json');

module.exports = router => {

	router.get('/', (req, res) => res.end('Welcome to Learn2Crack !'));

	router.post('/authenticate', (req, res) => {

		const credentials = auth(req);

		if (!credentials) {

			res.status(400).json({ message: 'Invalid Request !' });

		} else {

			login.loginUser(credentials.name, credentials.pass)

			.then(result => {

				const token = jwt.sign(result, config.secret, { expiresIn: 1440 });

				res.status(result.status).json({ message: result.message, token: token });

			})

			.catch(err => res.status(err.status).json({ message: err.message }));
		}
	});

	router.post('/users', (req, res) => {

		const name = req.body.name;
		const email = req.body.email;
		const password = req.body.password;

		if (!name || !email || !password || !name.trim() || !email.trim() || !password.trim()) {

			res.status(400).json({message: 'Invalid Request !'});

		} else {

			register.registerUser(name, email, password)

			.then(result => {

				res.setHeader('Location', '/users/'+email);
				res.status(result.status).json({ message: result.message })
			})

			.catch(err => res.status(err.status).json({ message: err.message }));
		}
	});

	router.get('/users/:id', (req,res) => {

		if (checkToken(req)) {

			profile.getProfile(req.params.id)

			.then(result => res.json(result))

			.catch(err => res.status(err.status).json({ message: err.message }));

		} else {

			res.status(401).json({ message: 'Invalid Token !' });
		}
	});

	router.put('/users/:id', (req,res) => {

		if (checkToken(req)) {

			const oldPassword = req.body.password;
			const newPassword = req.body.newPassword;

			if (!oldPassword || !newPassword || !oldPassword.trim() || !newPassword.trim()) {

				res.status(400).json({ message: 'Invalid Request !' });

			} else {

				password.changePassword(req.params.id, oldPassword, newPassword)

				.then(result => res.status(result.status).json({ message: result.message }))

				.catch(err => res.status(err.status).json({ message: err.message }));

			}
		} else {

			res.status(401).json({ message: 'Invalid Token !' });
		}
	});

	router.post('/users/:id/password', (req,res) => {

		const email = req.params.id;
		const token = req.body.token;
		const newPassword = req.body.password;

		if (!token || !newPassword || !token.trim() || !newPassword.trim()) {

			password.resetPasswordInit(email)

			.then(result => res.status(result.status).json({ message: result.message }))

			.catch(err => res.status(err.status).json({ message: err.message }));

		} else {

			password.resetPasswordFinish(email, token, newPassword)

			.then(result => res.status(result.status).json({ message: result.message }))

			.catch(err => res.status(err.status).json({ message: err.message }));
		}
	});

	function checkToken(req) {

		const token = req.headers['x-access-token'];

		if (token) {

			try {

  				var decoded = jwt.verify(token, config.secret);

  				return decoded.message === req.params.id;

			} catch(err) {

				return false;
			}

		} else {

			return false;
		}
	}
}

Creating app.js

This is the main file which starts our Node.js server. Here we use the middle ware body-parser and morgan with Express. The router is set to express by,

app.use(‘/api/v1’, router);

The /api/v1 is appended before each API endpoint.

Our server will run on port 8080.

app.js

'use strict';

const express    = require('express');        
const app        = express();                
const bodyParser = require('body-parser');
const logger 	   = require('morgan');
const router 	   = express.Router();
const port 	   = process.env.PORT || 8080;

app.use(bodyParser.json());
app.use(logger('dev'));

require('./routes')(router);
app.use('/api/v1', router);

app.listen(port);

console.log(`App Runs on ${port}`);

Running the Server

Open the Terminal or Command Prompt and switch to this node-login directory. Enter the following command to start the server.

node app

Open the browser and hit the url http://127.0.0.1:8080/api/v1/ to see the welcome message.