NodeJS Authentication Methods (Part 2)

Published Nov 26, 2017
NodeJS Authentication Methods (Part 2)

In the first part of this article, we addressed session based authentication. This time, we will be considering the second method of authentication listed in Part 1.

II. Token Based Authentication


In this method of authentication, we will use JWT (JSON web tokens) as a means of authentication.
JWT as defined on JWT.io are an open industry standard method for representing claims (could be a claim that you are a user and also and admin) securely between two parties. JWT can be generated, verified and decoded.

Tokens are stateless, i.e they don't get stored on the server like session, instead, they are stored on the clients. This gives room to scalability as we can add more machine and server deployments without having to worry about which of our server instances did our user logged in to.

Below is how authentication is done with tokens

  1. User Registers with email/username & password (password gets hashed at signup)
  2. User logs in using username and password (we query database for username/email and use bcrypt to comare stored passwordHash with user supplied password).
  3. If successful, a signed token is sent to the client application
  4. Client stores token in memory, locastorage, etc and sends it along with every request to the server via header.
  5. Server verifies token and only respond with data if token is valid.

To keep up, get the initial code structure from this article's github repo
Run these in your terminal/cmd (hit return/enter after each line)

git clone https://github.com/jalasem/nodeAuthsTuts.git
cd nodeAuthTuts
git checkout part2

here is the app.js code (well commented to explain how token-based authentication works)

// define dependencies
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const shortid = require('shortid');
const jwt = require('jsonwebtoken'); //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('./models/user'); // we shall create this (model/user.js) soon 

// configure bodyParser
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
let token_secret = 'iy98hcbh489n38984y4h498'; // !!! don't put this into your code at production.  Try using saving it into environment variable or a config file.

/*
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
                // 1. here our payload contains data we want client to hold for when next they send us any request
                const payload = {
                  email: userData.email,
                  username: userData.username,
                  id: userData._id
                }
                // 2. then we use jwt to sign our payload with our secret defined on line 23
                let token = jwt.sign(payload, token_secret);
                // 3. lastly we send the token and some other info we feel clients might need to them in form of response
                res.status(200).send({token, email: userData.email, username: userData.username})
            } 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 (take note of next() ) for it. Any endpoint that come after the authorization middleware won't pass if user doesn't have a valid token

Normally your server is expecting it as either an header 'x-access-token' or in the body of your request as 'token'
*/
app.use((req, res, next) => {
  if (req.session.user) {
    next();
  } else {
    res.status(401).send('Authrization failed! Please login');
  }
});
app.use((req, res, next) => {
  // check for token in the header first, then if not provided, it checks whether its supplied in the body of the request
  var token = req.headers['x-access-token'] || req.body.token
  if (token) {
    jwt.verify(token, token_secret, function (err, decoded) {
      if (!err) {
        req.decoded = decoded; // this add the decoded payload to the client req (request) object and make it available in the routes
        next();
      } else {
        res.status(403).send('Invalid token supplied');
      }
    })
  } else {
    res.status(403).send('Authorization failed! Please provide a valid token');
  }
})

app.get('/protected', (req, res) => {
  res.send(`You have access to this because you have supplied a valid token.
    	Your username is ${req.decoded.username}
        and email is ${req.decoded.email}.
    `)
})

/*
4. Logout
=============
*/
// Since JWT is stateless i.e server doesn't keep track of tokens/session, all you need to do to logout is just to delete the saved token at the client side. Since you can't make a request without a token, then you are logged out technically.

/*
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}`)
})

Pros of token based authentication

  1. It supports and enhance scalability
    Several server instances/clusters can be deployed and run simaultaneously without bothering about where each client is logged in. This will save memory to a reasonable extent, since we are not keeping track of sessions anymore.
    It enables you to authenticate your users accross multiple platforms, domain and subdomains wihtout a hitch. So it's a sure bet for scalability.

  2. Security
    This methods is relatively more secure than other methods, since it's stricly token based, tokens become invalid the moment users alter the data in it, since they don't have the token secret (so keep your token secret save!). It also guide against CSRF attacks. We could also enhance our security by setting expiration for our tokens.

  3. Ability to sell, lease, offer API as a service.
    With token, we can sell API access to our apps via token and even set request limits, users can give access to other application to perform action on their behalf on our platform through token. E.g You are building a reminder/todo application. But you want to give an app permission to remind you of all your facebook friends birthdays. We could do that with tokens. The possibilities are endless.

That's a comprehensive peek on Token based authentication, in the next/final part, we will be examining Passwordless authentication.

Stay productive and write your code for good.

Discover and read more posts from Abdul-Samii Ajala
get started