NodeJS Authentication Methods (Part 1)

November 25, 2017 0 Comments

NodeJS Authentication Methods (Part 1)

 

 

Authentication is an essential part of web development, we can't afford to undermine security.

Authentication is meant for the identification of users and provision of access rights and contents depending on their id.

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, We will create public, as well secret endpoints and 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 mesaures 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 sign up process, sign in, authorization, logout and password reset

Before we look into them one by one, lets 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 getting more popularly used 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 in it,
and 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, 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 wont 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 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


Tag cloud