Newby Coder header banner

NodeJs Basic Authentication Server

Basic 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 Basic 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 Basic Authentication Server for Mobile applications

This has following parts:

NodeJs Server with Basic 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('Message from Express server');
});

// 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 --save

Project structure

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

Sub directories are not created for simplicity

Implementation

User service

Role of service is to provide user data to the controller methods

Example provided here has a hardcoded json list

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

const users = [
  { id: 1, username: 'test', password: 'test', firstName: 'Test', lastName: 'User', phone: '5235234132' },
  { id: 2, 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) {
        const { id, ...otherUserData } = user;
        return {id: id, username: username};
    }
}

async function getUserInfo(req, res) {
    const user = users.find(u => u.id = req.user.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 api

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

router.post() assigns controller method for a route which acceps Http Post requests and router.get() for Get Apis

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) {
  // password is expected to be in base64 encoded form, which is decoded to send raw password to 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 'id' of user
      .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));
}

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

next can be thought of to be similar to Python's continue statement

Authorization string is expected to be present in a http header named Authorization and in the formBasic base64_encoded(username:password) where base64_encoded denotes that the concatenation username:passwordis encoded to base64 format

const userService = require('./user_service');
const async = require('async');


module.exports = authMiddleware;

async function authMiddleware(req, res, next) {
    // make authenticate path public i.e. skip authMiddleware middleware for path
    if (req.path === '/users/authenticate') {
        next();
    }

    // check for basic auth header
    if (!req.headers.authorization || req.headers.authorization.indexOf('Basic ') === -1) {
        return res.status(401).json({ message: 'Missing Authorization Header' });
    }

    // verify auth credentials
    const base64Credentials =  req.headers.authorization.split(' ')[1];
    const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
    const [username, password] = credentials.split(':');

    const user = await userService.authenticate( username, password );
    if (!user) {
        return res.status(401).json({ message: 'Invalid Authentication Credentials' });
    }

    // attach user to request object
    req.user = user

    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')
    function authReq() {
      output.innerHTML = ""
      username = document.getElementById('id_username').value
      password = document.getElementById('id_password').value
      const encoded =  btoa(password);
      fetch('http://127.0.0.1:4500/users/authenticate', {
        method: 'POST',
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({username: username, password: encoded})
      })
      .then(response => response.json())
      .then((value) => {
        output.innerHTML =  JSON.stringify(value)
      });
    }
    function getInfo() {
      output.innerHTML = ""
      const encoded = 'Basic ' + btoa(username+":"+password)
      fetch('http://127.0.0.1:4500/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)
      });
    }
    document.getElementById('btn').onclick = authReq
    document.getElementById('infoBtn').onclick = getInfo
  </script>
</html>

Test with username and password set in server

cl-nodejs-server-basic-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