Newby Coder header banner

NodeJs Jwt Authentication Server

Jwt Authentication is when raw(or with basic encoding) username and password is sent to the server, typically in its body

Provided example sets up a simple NodeJs server which exposes API (url) for Jwt HTTP Authentication

Check example Android/iOS mobile applications made in Flutter or React Native which sends request to the NodeJs server for authentication

Also check example of providing user data after authentication

NodeJs Jwt Authentication for Mobile applications

This has following parts:

NodeJs Server with Jwt Http Authentication

Provided example sets up a nodejs express server

Requirements

Express Server

Express is a Nodejs web application framework

Among other things, it is used to add middlewares to a web app, so that every request passes through these middlewaresallowing various functionalities such as request body parsing, authentication etc and also custom middlewares

Some middlewares like authentication might return a response and stop further processing of a request

//import express
const express = require('express');
// import external module to be used as middleware
const bodyParser = require('body-parser');
// import custom middleware
const authMiddleware = require('auth/auth_middleware');

// declare an express app
const app = express();

// add bodyParser.json() middleware to application to parse body of each request to json
// so that any further middleware or method can access request body as json
app.use(bodyParser.json());

// use custom authentication middleware
app.use(authMiddleware);

// assign a function to the route(or path) '/base', which only accepts Http Get requests (for Post request use app.post())
app.get('/base',function(req, res, next)
{
    // Send a string as response, can be changed to html or json content
    res.send('Hello World!');
});

// start server at port 8800
const server = app.listen(8800, function () {
    // function called after server is started
    console.log('Server listening on port ' + port);
});

What is next

next is a part of connect middleware

Middlewares are added as a stack

Each middleware calls next() which invokes the next middleware

Middlewares are executed one by one for each request to the app, until a middleware does not call next() within it

This flow is also stopped if next is called with an argument next(err) which is considered as an error

These can be tested to check if they behave differently, but maybe prefer altering a request req variable to send data to next middleware than trying with next(some_object)

Creating new Node.js Application

Create a new directory

mkdir nodejs_auth

Change directory to newly created directory

cd nodejs_auth

Run npm init command to create a package.json file so that node packages can be installed

npm init -y

-y is added so that it doesn't prompt for setting some common info about package


Dependency

Run following command from project directory

npm install async cors express rootpath jsonwebtoken --save

Project structure

Using lib as folder name to store backend files is like a convention

Configs are typically kept in separate directories

Implementation

User service

Role of service during authentication is to sign an object containing some information about a user and return a json web token

This token is returned to the client such as a mobile app, which sends it in subsequent requests to the server which are validated by authentication middleware

Example provided here has a hardcoded json list containing user data

Typically, a service can get data from database or other services

const jwt = require('jsonwebtoken')
const config = require('../config/config.json')

const users = [
  { id: 81623, username: 'test', password: 'test', firstName: 'Test', lastName: 'User', phone: '5235234132' },
  { id: 26858, username: 'ncuser', password: 'ncpassword', firstName: 'En', lastName: 'Cee', phone: '7685496767' }];

module.exports = {
    authenticate,
    getUserInfo
};

async function authenticate(username, password) {
    const user = users.find(u => u.username === username && u.password === password);
    if (user) {
        // use jsonwebtoken module to sign an object containing id & username of a user and get a corresponding token
        // alternatively, whole user object can be passed, but some specific fields can be selected to make it lightweight
        const token = jwt.sign({id: user.id}, config.privateKey, { expiresIn: config.tokenExpirySla})
        // return token to be forwarded to client
        return {token:token};
    }
}

async function getUserInfo(req, res) {
    // id of decoded field is used to get corresponding user object
    const user = users.find(u => u.id = req.decoded.id);
    const { password, ...userWithoutPassword } = user;
    res.json(userWithoutPassword);
}

Controller

The controller assigns methods to specific routes for express Router

These routes are the url paths that a client uses to call specific Apis

Instance of Express framework's Router is retrieved by calling express.Router()

router.post() assigns controller method for a route which accepts Http Post requests and router.get() for GET Api

const express = require('express');
// get Router instance
const router = express.Router();
// variable to access exported methods of user_service.js
const userService = require('./user_service');

// set controller methods for routes
router.post('/authenticate', authenticate);
router.get('/getInfo', getInfo);

module.exports = router;

function authenticate(req, res, next) {
  console.log("authenticate ", req.body)
  // password is expected to be in base64 encoded form, which is decoded to send raw password to user_service
  var password = Buffer.from(req.body.password, 'base64').toString('ascii')
  userService.authenticate(req.body.username, password)
      // here 'user' is the object returned from authenticate() if not null, which contains jwt token received from user_service
      .then(user => user ? res.json(user) :
          res.status(400).json({ message: 'Username or password is incorrect' }))
      // error is handled by global handler
      .catch(err => next(err));
}

// since this method is called after authentication middleware,
// the req (request) object has a 'decoded' field containing id of a user
function getInfo(req, res, next) {
  userService.getUserInfo(req, res)
}

Error Handler

Declare an error Handler to be used globally for the web app

Error handlers take 4 arguments, where first arg err is the error

module.exports = errorHandler;

function errorHandler(err, req, res, next) {
    if (typeof (err) === 'string') {
        // custom application error
        return res.status(400).json({ message: err });
    }

    // default to 500 server error
    return res.status(500).json({ message: err.message });
}

Authentication Middleware

Middlewares take 3 arguments and call next() when they are done processing for a request so that subsequent middleware can be executed for the request

A http header of name Authorization is expected to contain the concatenation of string Bearer and a jwt token

The concatenated string is splitted to extract the token and verified using jsonwebtoken module

If no error is received then it returns the object signed by user_service which is passed in the request

Alternative to Authorization, x-access-token header is also used by some to send json web tokens

It can be also sent as cookies based on how server and client applications implement it

const userService = require('./user_service');
const async = require('async');
const config = require('../config/config.json')
const jwt = require('jsonwebtoken')


module.exports = authMiddleware;

async function authMiddleware(req, res, next) {
    // make authenticate path public
    console.log(JSON.stringify(req.headers))
    if (req.path === '/users/authenticate') {
        return next();
    }

    // check for token
    const token = req.headers['x-access-token'] ||
      (req.headers.authorization && req.headers.authorization.indexOf('Bearer ') != -1
        && req.headers.authorization.split(' ')[1])
    if (!token) {
      return res.status(403).send({
          "error": true,
          "message": 'No token provided.'
      });
    }

    // verifies for privateKey and checks expiry to return decoded payload
    jwt.verify(token, config.privateKey, function(err, decoded) {
        if (err) {
            // send error response if err on validation
            return res.status(401).json({"error": true,
              "name": err.name? err.name : "Token verification failed",
              "message": err.message? err.message : 'Token verification failed' });
        }
      req.decoded = decoded;
      // continue to router method when no error
      next();
    });
}

index.js - Server entry point

index.js can be renamed to some other file, provided it corresponds to main of package.json

require('rootpath')();
const express = require('express');
const app = express();
const cors = require('cors');
const bodyParser = require('body-parser');
const errorHandler = require('lib/error_handler');
const authMiddleware = require('lib/middleware');

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

app.use(authMiddleware);

// api routes
app.use('/users', require('lib/controller'));

// global error handler
app.use(errorHandler);

// configure port number based on whether server is configured as running in production
// to set NODE_ENV as 'production'
const port = process.env.NODE_ENV === 'production' ? 80 : 4000;

// start server
const server = app.listen(port, function () {
    console.log('Server listening on port ' + port);
});

Run instructions

Run following command from project directory

nodejs index.js

To test running in production

sudo NODE_ENV='production' nodejs index.js

To make it as accessible over a local network such as wifi, enter local ip address with command

nodejs index.js 192.168.43.34

Here, 192.168.43.34 is ip address

Testing using html file

Save following code in a file named test.html (or some other name with .html extension) in a directory

Replace url in case server is run in some ip address other that localhost

Open the file in a browser (double-click should do that)

<!doctype html>
<html>
  <meta charset="utf-8">
  <body>
    <div>
      <div>
      <h3>Login Form</h3>
      <table>
        <tr>
          <td><label for="id_username">Enter username</label></td>
          <td><input type="text" id="id_username"></td>
        </tr>
        <tr>
          <td><label for="id_username">Enter password</label></td>
          <td><input type="password" id="id_password"></td>
        </tr>
        <tr>
          <td><button id='btn'>Authenticate</button></td>
          <td><button id='infoBtn'>Get user info</button></td>
        </tr>
      </table>
      <pre><code id="output"></code></pre>
      </div>
    </div>
  </body>
  <script>
    output = document.getElementById('output')
    token = null
    function authReq() {
      output.innerHTML = ""
      username = document.getElementById('id_username').value
      password = document.getElementById('id_password').value
      const encoded =  btoa(password);
      fetch('http://192.168.43.34:4000/users/authenticate', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({username: username, password: encoded})
      })
      .then(response => response.json())
      .then((responseJson) => {
        output.innerHTML =  JSON.stringify(responseJson)
        token = responseJson.token
      });
    }
    function getInfo() {
      output.innerHTML = ""
      const encoded = 'Bearer ' + token
      fetch('http://192.168.43.34:4000/users/getInfo', {
        method: 'GET',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
          'Authorization': encoded
        },
      })
      .then(response => response.json())
      .then((value) => {
        output.innerHTML =  JSON.stringify(value, null, 4)
      });
    }
    document.getElementById('btn').onclick = authReq
    document.getElementById('infoBtn').onclick = getInfo
  </script>
</html>

Test with username and password set in server

cl-nodejs-server-jwt-auth


Client Mobile Applications

The corresponding server for mobile applications uses an updated version of above code which adds some more fields to user data and adds another api

Check Adding Api to get user data