REST API Security with Node-Express-PostgreSQL-Oauth2

by Didin J. on Oct 09, 2020 REST API Security with Node-Express-PostgreSQL-Oauth2

A comprehensive step by step tutorial on building REST API security using Node.js, Express.js, PostgreSQL, Sequelize, Passport and Oauth2

Until now, OAuth2 is still very popular in REST API, Web, and Mobile App development for security or authentication. So, we will rewrite the comprehensive tutorial on building REST API security using Node.js, Express.js, PostgreSQL, Sequelize, and Oauth2 using the Express-Oauth-Server module.

This tutorial is divided into several steps:

In this example, we will show you simple Oauth2 grant types that are mostly used in mobile or web applications. They are password and refresh token grant types. So, the Oauth2 endpoint can be '/oauth/token' with different grants values for password and refresh_token. Also, we will add the additional endpoint for signup and secure API-endpoint.

The following tools, frameworks, and modules are required for this tutorial:

  1. Node.js
  2. PostgreSQL Server
  3. Express.js
  4. Sequelize.js
  5. Express Oauth Server
  6. Terminal or Command Line
  7. Text Editor or IDE
  8. Postman

We assume that you have installed the PostgreSQL server in your machine or can use your own remote server (we are using PostgreSQL 12.3). Also, you have installed Node.js on your machine and can run `node`, `npm` or `yarn` command in your terminal or command line. Next, check their version by typing these commands in your terminal or command line.

node -v
v12.18.0
npm -v
6.14.7
yarn -v
1.22.5

You can watch the video tutorial on our YouTube channel here. Please like, share, comment, and subscribe to ou YouTube channel.

That is the version that we are using. Let's continue with the main steps.


Step #1: Create Express.js Project and Install Required Modules

We will create a new Express.js application for REST API using Express Generator. Open your terminal or node command line the go to your projects folder. First, install express-generator using this command.

sudo npm install -g express-generator

Next, create an Express.js app using this command.

express express-oauth2-postgre --view=ejs

This will create Express.js project with the EJS view instead of the Jade view template because using the '--view=ejs' parameter. Next, go to the newly created project folder then install node modules.

cd express-oauth2-postgre && npm install

You should see the folder structure like this.

.
|-- app.js
|-- bin
|   `-- www
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
|   |-- images
|   |-- javascripts
|   `-- stylesheets
|       `-- style.css
|-- routes
|   |-- index.js
|   `-- users.js
`-- views
    |-- error.ejs
    `-- index.ejs

There's no view yet using the latest Express generator. We don't need it because we will create a REST API.


Step #2: Add Sequelize and Express Oauth Server Modules

Now, we will install all required modules such as Sequelize, PostgreSQL, Brcypt, Express Oauth Server (Node Oauth2 included), Body Parser, PostgreSQL. Type this command to install all of them.

npm install --save sequelize body-parser pg pg-hstore pg-promise bcrypt-nodejs express-oauth-server

Also, we need Sequelize-CLI to generate config, models, seeders, and migrations. For that, type this command to install Sequelize-CLI globally.

sudo npm install -g sequelize-cli

Next, create a new file at the root of the project folder.

touch .sequelizerc

Open and edit that file using your IDE or Text Editor then add these lines of codes.

const path = require('path');

module.exports = {
  "config": path.resolve('./config', 'config.json'),
  "models-path": path.resolve('./models'),
  "seeders-path": path.resolve('./seeders'),
  "migrations-path": path.resolve('./migrations')
};

That files will tell Sequelize initialization to generate config, models, seeders, and migrations files to specific directories.  Next, type this command to initialize the Sequelize.

sequelize init

That command will create `config/config.json`, `models/index.js`, `migrations`, and `seeders` directories and files. Next, open and edit `config/config.json` then make it like this.

{
  "development": {
    "username": "djamware",
    "password": "dj@mw@r3",
    "database": "express-oauth2",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "test": {
    "username": "root",
    "password": "dj@mw@r3",
    "database": "express-oauth2",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "production": {
    "username": "root",
    "password": "dj@mw@r3",
    "database": "express-oauth2",
    "host": "127.0.0.1",
    "dialect": "postgres"
  }
}

We use the same configuration for all the environment except the database name. Before running and test the connection, make sure you have created a database as described in the above configuration. You can use the `psql` command to create a user and database.

psql postgres --u postgres

Next, type this command for creating a new user with a password then give access for creating the database.

postgres-# CREATE ROLE djamware WITH LOGIN PASSWORD 'dj@mw@r3';
postgres-# ALTER ROLE djamware CREATEDB;

Quit `psql` then log in again using the new user that previously created.

postgres-# \q
psql postgres -U djamware

Enter the password, then you will enter this `psql` console.

psql (12.3)
Type "help" for help.

postgres=>

Type this command to creating a new database.

postgres=> CREATE DATABASE express_oauth2;

Then give that new user privileges to the new database then quit the `psql`.

postgres=> GRANT ALL PRIVILEGES ON DATABASE express_oauth2 TO djamware;
postgres=> \q


Step #3: Create or Generate Sequelize Models and Migrations

We will use Sequelize-CLI for generating a new model. Type this command to create a model for `Products` and `User` model for authentication.

sequelize model:create --name OAuthTokens --attributes accessToken:string,accessTokenExpiresAt:date,refreshToken:string,refreshTokenExpiresAt:date,clientId:integer,userId:integer
sequelize model:create --name OAuthClients --attributes clientId:string,clientSecret:string,redirectUris:string,grants:array
sequelize model:create --name OAuthUsers --attributes username:string,password:string,name:string

That command creates a model file to the model's folder and a migration file to the migrations folder. What we need is associations between models or tables and additional function inside the modules. Next, modify `models/oauthusers.js ` and then import this module.

var bcrypt = require('bcrypt-nodejs');

Add the new methods to the OAuthUsers model that convert the plain password to the encrypted password using Bscrypt, also, association with OAuthTokens. So, the `oauthusers.js` class will be like this.

'use strict';
const bcrypt = require('bcrypt-nodejs');
const { Model } = require('sequelize');

module.exports = (sequelize, DataTypes) => {
  class OAuthUsers extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      OAuthUsers.hasOne(models.OAuthTokens, {
        foreignKey: 'userId',
        as: 'token',
      });
    }
  };
  OAuthUsers.init({
    username: DataTypes.STRING,
    password: DataTypes.STRING,
    name: DataTypes.STRING
  }, {
    sequelize,
    modelName: 'OAuthUsers',
  });
  OAuthUsers.beforeSave((user) => {
    if (user.changed('password')) {
      user.password = bcrypt.hashSync(user.password, bcrypt.genSaltSync(10), null);
    }
  });
  return OAuthUsers;
};

Next, modify `models/oauthtokens.js` then add the association to OAuthClients and OAuthUsers. So, the whole OAuthTokens will look like this.

'use strict';
const {
  Model
} = require('sequelize');

module.exports = (sequelize, DataTypes) => {
  class OAuthTokens extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      OAuthTokens.belongsTo(models.OAuthClients, {
        foreignKey: 'clientId',
        as: 'client',
      });
      OAuthTokens.belongsTo(models.OAuthUsers, {
        foreignKey: 'userId',
        as: 'user',
      });
    }
  };
  OAuthTokens.init({
    accessToken: DataTypes.STRING,
    accessTokenExpiresAt: DataTypes.DATE,
    refreshToken: DataTypes.STRING,
    refreshTokenExpiresAt: DataTypes.DATE,
    clientId: DataTypes.INTEGER,
    userId: DataTypes.INTEGER
  }, {
    sequelize,
    modelName: 'OAuthTokens',
  });
  return OAuthTokens;
};

Next, modify `models/oauthclients.js` then add the association to OAuthTokens. So, the whole OAuthClients will look like this.

'use strict';
const {
  Model
} = require('sequelize');

module.exports = (sequelize, DataTypes) => {
  class OAuthClients extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      OAuthClients.hasOne(models.OAuthTokens, {
        foreignKey: 'clientId',
        as: 'token',
      });
    }
  };
  OAuthClients.init({
    clientId: DataTypes.STRING,
    clientSecret: DataTypes.STRING,
    redirectUris: DataTypes.STRING,
    grants: DataTypes.ARRAY(DataTypes.STRING)
  }, {
    sequelize,
    modelName: 'OAuthClients',
  });
  return OAuthClients;
};


Step #4: Create an Express Controller

We will put all overridden OAuth2 functions to the controller. For that, create a folder and controller file.

mkdir controllers
touch controllers/oauthcontroller.js

Next, import or declare all required models and the Bcrypt module.

const bcrypt = require('bcrypt-nodejs');
const OAuthTokensModel = require('../models').OAuthTokens;
const OAuthClientsModel = require('../models').OAuthClients;
const OAuthUsersModel = require('../models').OAuthUsers;

Export a module that overrides getAccessToken function to get the access token from the database model. The OAuthClientsModel and OAuthUsersModel as the requirement of the result data should contain client and user. Otherwise, the request will return error could not find AccessToken().

module.exports.getAccessToken = function (bearerToken) {
    return OAuthTokensModel.findOne(
        {
            where: {
                accessToken: bearerToken
            },
            include: [
                {
                    model: OAuthClientsModel,
                    as: 'client'
                },
                {
                    model: OAuthUsersModel,
                    as: 'user'
                }
            ]
        })
        .then((token) => {
            const data = new Object();
            for (const prop in token.get()) data[prop] = token[prop];
            data.client = data.client.get();
            data.user = data.user.get();
            return data;
        })
        .catch((error) => console.error(error));
};

Export a module that overrides the getClient function to get the client data from the database model. We add options raw: true to make the query return as a raw object. Otherwise, the Node-Oauth2-Server could not find the client.

module.exports.getClient = function (clientId, clientSecret) {
    return OAuthClientsModel.findOne({where: {clientId: clientId, clientSecret: clientSecret}, raw: true});
};

Export a module that overrides the getRefreshToken function to get the refresh token from the database model. This time, we add the include options to get the association model of OAuthClientsModel and OAuthUsersModel then build the response with the additional client and user fields. That additional field is required by Node-Oauth2-Server to make getRefreshToken function working.

module.exports.getRefreshToken = function (refreshToken) {
    return OAuthTokensModel.findOne(
        {
            where: {
                refreshToken: refreshToken
            },
            include: [
                {
                    model: OAuthClientsModel,
                    as: 'client'
                },
                {
                    model: OAuthUsersModel,
                    as: 'user'
                }
            ]
        })
        .then((token) => {
            const data = new Object();
            for (const prop in token.get()) data[prop] = token[prop];
            data.client = data.client.get();
            data.user = data.user.get();
            console.log(data);
            return data;
        })
        .catch((error) => console.error(error));
};

Export a module that overrides the getUser function to get the user from the database model. There are 2 steps to query a user, they are by username then execute Bcrypt compares the password with the password from the found user.

module.exports.getUser = function (username, password) {
    return OAuthUsersModel.findOne({where: {username: username}})
        .then((user) => {
            const isMatch = bcrypt.compareSync(password, user.get().password);
            if (isMatch) {
                return user.get();
            } else {
                console.error("Password not match");
            }
        });
};  

Export a module that overrides the save token function to save the accessToken and refreshToken to the database model. This function depends on previous functions (getClient and getUser). The token generated by the Node-Oauth2-Server module.

module.exports.saveToken = function (token, client, user) {
    return OAuthTokensModel
        .create(
            {
                accessToken: token.accessToken,
                accessTokenExpiresAt: token.accessTokenExpiresAt,
                clientId: client.id,
                refreshToken: token.refreshToken,
                refreshTokenExpiresAt: token.refreshTokenExpiresAt,
                userId: user.id
            }
        )
        .then((token) => {
            const data = new Object();
            for (const prop in token.get()) data[prop] = token[prop];
            data.client = data.clientId;
            data.user = data.userId;

            return data;
        })
        .catch((error) => console.error(error));
};

As you can see, there are the additional client and user fields required by Node-Oauth2-Server. Next, export a module that overrides the revokeToken which use to get refresh token.

module.exports.revokeToken = function (token) {
    console.log("Revoke token");
    return OAuthTokensModel
        .findOne({where: {refreshToken: token.refreshToken}})
        .then(refreshToken => {
            console.log(refreshToken);
            return refreshToken
                .destroy()
                .then(() => {
                    return !!refreshToken
                })
                .catch((error) => console.error(error));
        })
        .catch((error) => console.error(error));
};

Next, export the additional functions that save a client and user data to the database.

module.exports.setClient = function (client) {
    return OAuthClientsModel
        .create({
            clientId: client.clientId,
            clientSecret: client.clientSecret,
            redirectUris: client.redirectUris,
            grants: client.grants,
        })
        .then((client) => {
            client = client && typeof client == 'object' ? client.toJSON() : client;
            const data = new Object();
            for (const prop in client) data[prop] = client[prop];
            data.client = data.clientId;
            data.grants = data.grants;

            return data;
        })
        .catch((error) => console.error(error));
};

module.exports.setUser = function (user) {
    return OAuthUsersModel
        .create({
            username: user.username,
            password: user.password,
            name: user.name
        })
        .then((userResult) => {
            userResult = userResult && typeof userResult == 'object' ? userResult.toJSON() : userResult;
            const data = new Object();
            for (const prop in userResult) data[prop] = userResult[prop];
            data.username = data.username;
            data.name = data.name;

            return data;
        })
        .catch((error) => console.error(error));
};


Step #5: Implementing OAuth2 REST API Endpoint

We will use an existing Express route to implement the Oauth2 REST API endpoint. Open and edit `routes/index.js` then add these imports or required modules.

const express = require('express');
const router = express.Router();
const OauthController = require('../controllers/oauthcontroller');
const OAuthServer = require('express-oauth-server');

Add the main declaration to the Express Oauth Server module with the model using OauthController that previously created.

router.oauth = new OAuthServer({
    model: OauthController
});

Add the main REST API route to get the access token or log in.

router.post('/oauth/token', router.oauth.token());

Add a route to save new client data.

router.post('/oauth/set_client', function (req, res, next) {
    OauthController.setClient(req.body)
        .then((client) => res.json(client))
        .catch((error) => {
            return next(error);
        });
});

Add a route to save a new user data.

router.post('/oauth/signup', function (req, res, next) {
    OauthController.setUser(req.body)
        .then((user) => res.json(user))
        .catch((error) => {
            return next(error);
        });
});

Add a route for a secure endpoint. You can add another secure endpoint determine by router.oauth.authenticate() function.

router.get('/secret', router.oauth.authenticate(), function (req, res) {
    res.json('Secret area');
});


Step #6: Run and Test Secure Node.js, Express.js, PostgreSQL, and Oauth2

We will use Postman to test this secure Node.js, Express.js, PostgreSQL, and Oauth2 REST API. First, make sure the PostgreSQL server is running on your machine then run this Express application from the terminal or command line.

nodemon

or 

npm start

Start the Postman application the test to request to the secure endpoint.

REST API Security with Node-Express-PostgreSQL-Oauth2  - postman secret area

That request should return a response with 401 status or Unauthorized. Next, save client data to make this Oauth2 work.

REST API Security with Node-Express-PostgreSQL-Oauth2  - postman set client

Register a new user using this request.

REST API Security with Node-Express-PostgreSQL-Oauth2  - postman signup

Sign in to the Oauth2 server to get the access token. We will use Basic Auth as Authorization using the username and password that previously save to the client table. Also, add the x-www-form-urlencoded as the body that contains username, password, and grant_type.

REST API Security with Node-Express-PostgreSQL-Oauth2  - postman signin 1
REST API Security with Node-Express-PostgreSQL-Oauth2 - postman signin 2

Back to the secure area, add this header with additional value prefix "Bearer ".

REST API Security with Node-Express-PostgreSQL-Oauth2  - postman secret with token

To refresh token, use the same postman parameters as sign in or get access token. Replace the request body with this.

REST API Security with Node-Express-PostgreSQL-Oauth2  - postman refresh token

That it's, the simple implementation of Oauth2 in Node, Express, and PostgreSQL REST API. You can get the full working source code from our GitHub.

That's just the basic. If you need more deep learning about Node.js, Express.js, PostgreSQL, Vue.js, and GraphQL or related you can take the following cheap course:

Thanks!