Featured image of post Securing Your Node.js & Express App with Auth0

Securing Your Node.js & Express App with Auth0

How to install Auth0 in NodeJS MVC and protect the app against OWASP Top 10 vulnerabilities

In this comprehensive guide, we’ll explore in-depth strategies and best practices to secure a Node.js and Express web application against critical security threats outlined in the OWASP Top 10 list.

You can view the git repository that I implemented all the things here in blog post here.

And the hosted web application here.

We’ll build a sample Node.js MVC app with Express and implement robust authentication via Auth0. Then we’ll walk through measures to defend against:

  • Broken access control & authentication
  • SQL Injection attacks
  • Cross-site scripting (XSS)
  • Cross-site request forgery (CSRF)
  • Sensitive data exposure
  • Captchas for rate limiting and bot prevention

By the end, you’ll have a hardened Express app fortified against the most prevalent web app vulnerabilities. Let’s dive in!

Auth0 Authentication

Broken authentication and access control issues compromise app security. Auth0 provides a secure authentication and authorization layer for Express apps.

First, sign up for a free Auth0 account.

Then install express-openid-connect to connect your application with Auth0:

const { auth } = require("express-openid-connect");

app.use(
  auth({
    issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL,
    baseURL: process.env.BASE_URL,
    clientID: process.env.AUTH0_CLIENT_ID,
    secret: process.env.SESSION_SECRET,
    authRequired: false,
    auth0Logout: true,
  })
);

This gives access to check auth0 authentication status for all requests incoming. We can use a simple if-else check to see whether the request is truly authenticated.

Example for a protected rout:

router.get("/", async (req, res) => {
//Below if conndition checks for authentication:
  if (req.oidc.isAuthenticated()) {
   // Render the requested content that belongs to the user
   
  } else {
  // redirecting to login page as he is not authenticated:
    res.redirect("/login");
  }
});

This provides a easy and robust authentication and authorization for routes.

SQL Injection Defense

For the data layer, we’ll use Node.js with MySQL. SQL injection is one of the most dangerous vulnerabilities. To prevent it we use an ORM called, Sequalize.

To install Sequalize run: npm install sequelize sequelize-cli mysql2

As this ORM treats all the user input data as strings-not as SQL commands-the ORM prevents the vulnerability of SQL injection. Below code demonstrate how to implement this properly.

For any DB operation using this ORM, you must create a model beforehand.

//Example model: 

const { DataTypes } = require("sequelize");
const database = require("../config/DBConnection").dbConnection;

const Reservation = database.define(
  "Model1",
  {
    booking_id: {
      type: DataTypes.INTEGER,
      allowNull: false,
      primaryKey: true,
      autoIncrement: true,
    },
    date: {
      type: DataTypes.DATE,
      allowNull: false,
    },
    username: {
      type: DataTypes.STRING(255),
      allowNull: false,
    },
  },
  {
    tableName: "tNAme",
    timestamps: false, 
  }
);

1. Use Sequalize for insertion :

const Model1 = require("../models/Model1");

const newReservation = await Reservation.create(reservationData);
// Above line inserts data into the DB without being susceptible to SQL injection.

2. Use Sequalize for data selection based on userName :

const data = await Model1.findAll({
      where: {
        username: {
          [Op.eq]: userName ,
        },
      },
    });
// In this way, attackers can't directly interfere the SQL query and manipulate that to inject code.

Preventing XSS

Input Sanitization: XSS attacks are launched through the user inputs most of the time. Therefore we should use a library which sanitizes all the user inputs.

DOMPurify is one of those libraries.

Install it through: npm install dompurify jsdom

You can use it as below code:

const createDOMPurify = require("dompurify");
const { JSDOM } = require("jsdom");

const sanitize = (string) => {
  const window = new JSDOM("").window;
  const DOMPurify = createDOMPurify(window);
  return DOMPurify.sanitize(string);
};

router.post("/", async (req, res) => {
 const userInput= req.body;
//Below code sanitizes the whole user inputs we got from a form using the sanitizing library DOM Purify:
 
 for (const key in userInput) {
    userInput[key] = sanitize(userInput[key]);
  }
});

Rendering escaped data: The injected malicious code of the attacker is executed on the frontend. Therefore to prevent it from being rendered on the frontend we can use ejs template escaping (We use ejs template engine for NodeJS for this app) :

<span>
  <%= user.info %> 
</span> 

As we have used <%= for rendering, the data inside user.info will always be displayed as a plaintext even though they are HTML code.

So encoding will prevent malicious scripts from executing.

Blocking CSRF

Cross-site request forgery (CSRF) tricks users into making forged requests.

Anti-CSRF tokens are used to identify the authentic frontends and to block the unauthorized requests:

1. Using Anti-CSRF Tokens: First install csrf-csrf package

Then setup csrf-csrf package as follows in the starting point on your app: [ When this package is installed in this way, it checks all requests from all the routes (except GET requests) for CSRF token validation ]

const { doubleCsrf } = require("csrf-csrf");

const {
  invalidCsrfTokenError,
  generateToken,
  validateRequest,
  doubleCsrfProtection,
} = doubleCsrf({
  getSecret: () => {
    return "someSecretValue";
  },
  getTokenFromRequest: (req) => {
    return req.body._csrf;
    //_csrf is the CSRF token's input name in a form element
  },
  cookieName: "doubleCrsf",
  cookieOptions: {
    sameSite: "lax",
    path: "/",
    secure: false,
  },
});

app.use(doubleCsrfProtection);

Then use CSRF token in the rendered EJS form input as a hidden element: The value csrfToken returns when req.csrfToken() is called of an incoming request.

<span>
  <input type="hidden" name="_csrf" value="<%= csrfToken %>">
</span> 

In this way we can be adamant that the frontend which is sending us data is authentic.

2. Enable CORS to block the untrusted origins:

const cors = require('cors');

const allowedOrigins = ['https://example.com'];

app.use(cors({
  origin: function(origin, callback){
    if(!origin || allowedOrigins.indexOf(origin) !== -1){
      callback(null, true);
    } else {
      callback(new Error('Not allowed')); 
    }
  }
}));

Now requests without an allowed origin will be rejected.

These measures will effectively block CSRF attacks.

Bots detection

We can easily detect the bots using captchas when receiving user inputs.

First install hcaptcha package and sign up for their website and setup the secrets.

Then in the fontend use below code to show captcha widget to the users:

<head>
    <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
</head>
<body>
<form>
    <div class="h-captcha" data-sitekey="<%= hCapctchaKey %>">
</form>
</body>

Then use the code below to verify the captcha:

const hcaptcha = require("hcaptcha");
const dotenv = require("dotenv").config();

const secretKey = process.env.HCAPTCHA_SEC_KEY;

//Call verify function with the token for captacha verification.
async function verify(token) {
  try {
    const verified = await hcaptcha.verify(secretKey, token);

    if (verified) {
      return true;
    } else {
      return false;
    }
  } catch (error) {
    return false;
  }
}

In this way we can prevent the bot traffic easily.

Preventing DoS

For DDoS resistance, use a cloud firewall service like Cloudflare for your app.

Securing Sensitive Data

Don’t store plain text passwords or other sensitive data. Instead:

  1. Hash all passwords using bcrypt before storing.

  2. Encrypt other sensitive data like credit cards using AES-256 or similar.

  3. Restrict and monitor database access.

  4. Transmit sensitive data over HTTPS only.

These best practices will help secure critical user data.

Other Security Tips

  • Use Helmet middleware to set security headers
  • Enable HTTP Strict Transport Security
  • Remove fingerprinting headers like X-Powered-By
  • Run regular dependency audits with npm audit
  • Configure all middleware securely
  • Follow the principle of least privilege
  • Enable logging and monitoring

Conclusion

In summary, these best practices provide end-to-end security for Express apps against critical web application vulnerabilities.

Adopting these will result in a hardened, production-ready Node.js and Express application secured against the OWASP Top 10 and more.

While security requires constant vigilance, rigorously following these guidelines will go a long way in protecting your app as you build and scale.

Now go forth and ship more secure Node.js applications!

Built with Hugo
Theme Stack designed by Jimmy