Codementor Events

NodeJS Authentication Methods (Part 1)

Published Nov 21, 2017Last updated Jul 16, 2018
NodeJS Authentication Methods (Part 1)

Authentication is meant for the identification of users and provision of access rights and contents depending on their id.It is an essential part of web development that we can't afford to undermine its security.

In this article, we shall be looking at different methods/ways by which we can implement authentication in our NodeJS apps. It will be API based and we will create public, as well secret endpoints.Then,we shall use each of our authentication methods to implement the security of those endpoints.
This is meant to explain authentication methods. To implement it at production, extra security measures such as data validation have to be taken into consideration.

This article will be in two(2) parts and we will be considering the following authentication Methods

  • Session Based Authentication
  • Token Based Authentication
  • Passwordless Authentication

In this tutorial, we shall be examining five(5) key aspects of authentication in each methdods. These are the sign up process, the sign in, authorization, logout and password reset

Before we look into them one by one, let's setup our model API.

We shall be using the following node packages depending on the authentication method we choose to use.

  • express: a minimalist framework for building web applications
  • express-session: For managing session in your express application
  • bcrypt: encryption and decryption of passwords
  • bodyparser: for parsing data in the body of requests sent to the server
  • nodemailer: for sending email(password reset email in this case)
  • nodemailer-smtp-transport: to configure smtp transport setup for nodemailer
  • mongoose: A DRM (Data relation management) for managing mongodb database
  • jsonwebtoken: for implementing Token based authentication
  • shortid: for generating unique keys when implementing password reset

Our Server/Api

Requirements

  1. NodeJS have to be installed. Visit NodeJS.org to install on your machine
  2. Mongodb (I recommend mlab)
  3. Any command line environment (i.e Terminal, iTerm, cmd, powershell, git bash e.t.c)
  4. A text editor. I personally recommend Visual Studio code

I will be using es6 in this tutorial as it is used more popularly in the javascript community and also to stand the test of time

Our Project file structure

nodeAuthTut
  |-- models
    |    |-- user
    |-- app.js
    |-- package.json

So let's get started,

Let's do some command line exercises. Let's create our folder and files.

  • open your desired terminal
  • navigate to your desktop (or your desired workspace) cd Desktop
  • create our project folder, run mkdir nodeAuthTut && cd nodeAuthTut
  • create our models folder in our project folder, run mkdir models && cd models && touch user.js
  • change directory back into our root folder cd ..
  • run this in your root nodeAuthTut directory:
npm init -y
  • then we install our dependencies like so
npm i -S express body-parser express-session bcrypt nodemailer nodemailer-smtp-transport jsonwebtoken mongoose shortid
  • to create our app entry point, run touch app.js,
    here is our initial app.js code below:
// define dependencies
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const app = express();
const PORT = 3000; // you can change this if this port number is not available

//connect to database
mongoose.connect('mongodb://localhost:27017/auth_tuts', {
  useMongoClient: true
  } (err, db) => {
    if (err) {
      console.log("Couldn't connect to database");
    } else {
      console.log(`Connected To Database`);
    }
  }
);

// define database schemas
const user = require('./model/user'); // we shall create this (model/user.js) soon 

// configure bodyParser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

app.get('/', (req, res) => {
  res.send('Welcome to the Home of our APP');
})

app.get('/protected', (req, res) => {
  res.send('This page is protected. It requires authentication');
})

app.listen(PORT, () => {
  console.log(`app running port ${PORT}`)
})

in our model/user.js, we have this

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

// we create a user schema
let userSchema = new Schema({
  fullname: {
    type: String,
    required: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    trim: true,
    unique: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true
  },
  gender: {
    type: String,
    required: true
  },
  passResetKey: String,
  passKeyExpires: Number,
  createdAt: {
    type: Date,
    required: false
  },
  updatedAt: {
    type: Number,
    required: false
  },
}, {runSettersOnQuery: true}); // 'runSettersOnQuery' is used to implement the specifications in our model schema such as the 'trim' option.

userSchema.pre('save', function (next) {
  this.email = this
    .email
    .toLowerCase(); // ensure email are in lowercase

  var currentDate = new Date().getTime();
  this.updatedAt = currentDate;
  if (!this.created_at) {
    this.createdAt = currentDate;
  }
  next();
})

var user = mongoose.model('user', userSchema);

module.exports = user;

open your folder in any prefered text editor, copy the code above and paste it in.Before we examine the authentication methods one by one, update the script section of your package.json like so

{
  ...
    "script": {
    	"start": "node app.js"
    }
}

then run npm npm start
try http://localhost:3000 and http://localhost:3000/protected (or your configured port number) in your browser or postman. You will discover that both gives us a favourable response even though we are not authenticated to access the protected route.

Login Screen
Photo credits: (Behnam Sobhkhiz)[https://www.uplabs.com/behnamsobhkhiz]

I. Session Based Authentication


In session based authentication, users credentials(username/email and password for example) are compared with what is stored in the database and if they match, a session is initialized for the user with the fetched id. These sessions are terminated on user logout and they are meant to expire after a configured time.

To implement this update your app.js like so

// define dependencies
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const shortid = require('shortid');
const session = require('express-session'); //we're using 'express-session' as 'session' here
const bcrypt = require("bcrypt"); // 
const app = express();
const PORT = 3000; // you can change this if this port number is not available

//connect to database
mongoose.connect('mongodb://localhost:27017/auth_tuts', { //replace this with you
  useMongoClient: true
  } (err, db) => {
    if (err) {
      console.log("Couldn't connect to database");
    } else {
      console.log(`Connected To Database`);
    }
  }
);

// define database schemas
const User = require('./model/user'); // we shall create this (model/user.js) soon 

// configure bodyParser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(
  session({
    secret: "iy98hcbh489n38984y4h498", // don't put this into your code at production.  Try using saving it into environment variable or a config file.
    resave: true,
    saveUninitialized: false
  })
);

/*
0. Unprotected route
=============
*/
app.get('/', (req, res) => {
  res.send('Welcome to the Home of our APP');
})

/*
1. User Sign up
=============
*/
// here we're expecting username, fullname, email and password in body of the request for signup. Note that we're using post http method
app.post('/signup', (req, res) => {
  let {username, fullname, email, password} = req.body; // this is called destructuring. We're extracting these variables and their values from 'req.body'
    
    let userData = {
    	username,
        password: bcrypt.hashSync(password, 5), // we are using bcrypt to hash our password before saving it to the database
        fullname,
        email
    };
    
    let newUser = new User(userData);
    newUser.save().then(error => {
    	if (!error) {
        	return res.status(201).json('signup successful')
        } else {
        	if (error.code ===  11000) { // this error gets thrown only if similar user record already exist.
            	return res.status(409).send('user already exist!')
            } else {
            	console.log(JSON.stringigy(error, null, 2)); // you might want to do this to examine and trace where the problem is emanating from
            	return res.status(500).send('error signing up user')
            }
        }
    })
})

/*
2. User Sign in
=============
*/
We will be using username and password, but it can be improved or modified (e.g email and password or some other ways as you please)
app.post('/login', (req, res) => {
  let {username, password} = req.body;
    User.findOne({username: username}, 'username email password', (err, userData) => {
    	if (!err) {
        	let passwordCheck = bcrypt.compareSync(password, userData.password);
        	if (passwordCheck) { // we are using bcrypt to check the password hash from db against the supplied password by user
                req.session.user = {
                  email: userData.email,
                  username: userData.username
                  id: userData._id
                }; // saving some user's data into user's session
                req.session.user.expires = new Date(
                  Date.now() + 3 * 24 * 3600 * 1000; // session expires in 3 days
                );
                res.status(200).send('You are logged in, Welcome!');
            } else {
            	res.status(401).send('incorrect password');
            }
        } else {
        	res.status(401).send('invalid login credentials')
        }
    })
})

/*
3. authorization
=============
A simple way of implementing authorization is creating a simple middleware for it. Any endpoint that come after the authorization middleware won't pass if user doesn't have a valid session
*/
app.use((req, res, next) => {
  if (req.session.user) {
    next();
  } else {
    res.status(401).send('Authrization failed! Please login');
  }
});

app.get('/protected', (req, res) => {
  res.send(`You are seeing this because you have a valid session.
    	Your username is ${req.session.user.username} 
        and email is ${req.session.user.email}.
    `)
})

/*
4. Logout
=============
*/
app.all('/logout', (req, res) => {
  delete req.session.user; // any of these works
  	req.session.destroy(); // any of these works
    res.status(200).send('logout successful')
})

/*
4. Password reset
=================
We shall be using two endpoints to implement password reset functionality
*/
app.post('/forgot', (req, res) => {
  let {email} = req.body; // same as let email = req.body.email
  User.findOne({email: email}, (err, userData) => {
    if (!err) {
      userData.passResetKey = shortid.generate();
      userData.passKeyExpires = new Date().getTime() + 20 * 60 * 1000 // pass reset key only valid for 20 minutes
      userData.save().then(err => {
          if (!err) {
            // configuring smtp transport machanism for password reset email
            let transporter = nodemailer.createTransport({
              service: "gmail",
              port: 465,
              auth: {
                user: '', // your gmail address
                pass: '' // your gmail password
              }
            });
            let mailOptions = {
              subject: `NodeAuthTuts | Password reset`,
              to: email,
              from: `NodeAuthTuts <yourEmail@gmail.com>`,
              html: `
                <h1>Hi,</h1>
                <h2>Here is your password reset key</h2>
                <h2><code contenteditable="false" style="font-weight:200;font-size:1.5rem;padding:5px 10px; background: #EEEEEE; border:0">${passResetKey}</code></h4>
                <p>Please ignore if you didn't try to reset your password on our platform</p>
              `;
            };
            try {
              transporter.sendMail(mailOptions, (error, response) => {
                if (error) {
                  console.log("error:\n", error, "\n");
                  res.status(500).send("could not send reset code");
                } else {
                  console.log("email sent:\n", response);
                  res.status(200).send("Reset Code sent");
                }
              });
            } catch (error) {
              console.log(error);
              res.status(500).send("could not sent reset code");
            }
          }
        })
    } else {
      res.status(400).send('email is incorrect');
    }
  })
});

app.post('/resetpass', (req, res) => {
  let {resetKey, newPassword} = req.body
    User.find({passResetKey: resetKey}, (err, userData) => {
    	if (!err) {
        	let now = new Date().getTime();
            let keyExpiration = userDate.passKeyExpires;
            if (keyExpiration > now) {
        userData.password = bcrypt.hashSync(newPassword, 5);
                userData.passResetKey = null; // remove passResetKey from user's records
                userData.keyExpiration = null;
                userData.save().then(err => { // save the new changes
                	if (!err) {
                    	res.status(200).send('Password reset successful')
                    } else {
                    	res.status(500).send('error resetting your password')
                    }
                })
            } else {
            	res.status(400).send('Sorry, pass key has expired. Please initiate the request for a new one');
            }
        } else {
        	res.status(400).send('invalid pass key!');
        }
    })
})

app.listen(PORT, () => {
  console.log(`app running port ${PORT}`)
})

We have seen how to implement a simple session based authentication and we can now implement login, sign up, authorization, logout and password reset/update.

We will finish up with the two other methods (token and passwordless auth) in the next part of this article. I will like your comments, questions and suuggestions in the comments section below

Discover and read more posts from Abdul-Samii Ajala
get started
post commentsBe the first to share your opinion
Aravind KR
5 years ago

I’m getting error while signup

JSON i/p:
“username”:“akr86”,
“password”: “akr167”,
email":"akr86@gmail.com”,
“fullname”:“AKR”

Error:

SyntaxError: Unexpected string in JSON at position 86
<br>    at JSON.parse (<anonymous>)
<br>    at parse (D:\nodejspractice\nodeAuthTut\node_modules\body-parser\lib\types\json.js:89:19)
<br>    at D:\nodejspractice\nodeAuthTut\node_modules\body-parser\lib\read.js:121:18
<br>    at invokeCallback (D:\nodejspractice\nodeAuthTut\node_modules\raw-body\index.js:224:16)
<br>    at done (D:\nodejspractice\nodeAuthTut\node_modules\raw-body\index.js:213:7)
<br>    at IncomingMessage.onEnd (D:\nodejspractice\nodeAuthTut\node_modules\raw-body\index.js:273:7)

Maarlon1
6 years ago

Thanks but if this is ‘simple’ I wonder how it looks when it’s complicated. in PHP it’s a few lines, in Node/JS it’s 200+ lines. is this a progress or…?

Abubakar M.I Abdu
6 years ago

Great job Abdul… It’ll sure come in handy

Show more replies