Streamlining Node.js Authentication with Auth0

Streamlining Node.js Authentication with Auth0

Introduction

As the world moves toward more interconnected and complex web applications, having a secure authentication system that can handle the complex authentication mechanisms required for modern applications has become critical. Auth0 is a well-known authentication provider that provides comprehensive solutions to assist developers in easily integrating authentication into their applications. We will look at how to use Auth0 for authentication in Node.js applications in this guide.

Auth0 Authentication with Node.js

Node.js is a popular server-side JavaScript runtime that is widely used to create scalable and robust web applications. Auth0 provides an SDK called auth0.js that allows developers to easily integrate Auth0 authentication into their Node.js applications. In this guide, we will look at how to use Auth0 with Node.js.

Prerequisites

Before we begin, ensure that you have the following installed on your system:

  • Node.js

  • Mongodb

  • Auth0 account

    Setting up Auth0

    To use Auth0 in our application, we need to set up an Auth0 account and create an application. Here are the steps to create an application in Auth0:

    1. Sign up for an Auth0 account if you haven't already.

    2. Once you are logged in to your account, click on the "Applications" tab on the dashboard and click on the "Create Application" button.

    3. Give your application a name and select "Regular Web Applications" as the application type.

    4. In the "Settings" tab of your application, add the following URLs to the "Allowed Callback URLs", "Allowed Logout URLs", and "Allowed Web Origins" fields:

       http://localhost:3000/callback,
       http://localhost:3000/logout,
       http://localhost:3000
      

      Replace localhost:3000 with your application URL if you are deploying your application to a remote server.

Project Setup

Create a new Node.js project by running the following command:

$ mkdir node-auth0
$ cd node-auth0
$ npm init -y

Install the required dependencies:

$ npm i express express-openid-connect mongoose ejs dotenv mime

Create a .env file and add the following environment variables:

SECRET=your_auth0_secret
BASE_URL=your_auth0_base_url
CLIENT_ID=your_auth0_client_id
ISSUER_BASE_URL=your_auth0_issuer_base_url
MONGODB_URI=your_mongodb_uri
  • ISSUER_BASE_URL - The Domain as a secure URL found in your Application settings

  • BASE_URL - The URL where the application is served (since its test you can make it localhost:300)

  • CLIENT_ID - The Client ID found in your Auth0 Application settings

  • SECRET - A long, random string minimum of 32 characters long

Replace the values with your credentials from Auth0

  • auth0.js
const { auth } = require('express-openid-connect')
require('dotenv').config()

const auth0Config = {
    authRequired: false,
    auth0Logout: true,
    secret: process.env.AUTH0_SECRET,
    baseURL: process.env.AUTH0_BASE_URL,
    clientID: process.env.AUTH0_CLIENT_ID,
    issuerBaseURL: process.env.AUTH0_ISSUER_BASE_URL
}

module.exports = auth(auth0Config)

The configuration settings for the Auth0 authentication. It uses the auth function from express-openid-connect to handle the authentication. The auth0Config object defines the configuration settings such as authRequired, auth0Logout, secret, baseURL, clientID, and issuerBaseURL. The module.exports statement exports the auth function with the auth0Config settings.

  • books.js
const mongoose = require('mongoose')

const Schema = mongoose.Schema;

const bookSchema = new Schema({
    title: {
        type: String,
        required: true
    },
    year: {
        type: Number,
        required: true,
        max: [2023, 'Year must be less than 2023']
    },
    price: {
        type: Number,
        required: true,
        min: [0, 'Price must be greater than 0']
    }},
    {timestamps: true}
)

module.exports= mongoose.model('Books', bookSchema);

This code defines the bookSchema object for the books collection in the database. The schema defines the properties of a book such as title, year, and price. The module.exports statement exports the Books model with the bookSchema.

  • book.controller.js
const bookModel = require("../models/books")

function getAllBooks(req, res) {
    bookModel.find()
        .then(books => {
            res.status(200).render('books', { books: books })
        })
        .catch(error => {
            console.error(error)
            res.status(500).send(error)
        })
}

function addBook(req, res) {
    const book = req.body
    bookModel.create(book)
     .then(books => {
         res.status(201).redirect('/books')
     })
     .catch(error => {
         console.error(error)
         res.status(500).send(error)
     })
}

module.exports = {
    getAllBooks,
    addBook
}

This code defines two functions for handling book requests: getAllBooks and addBook. The getAllBooks function retrieves all books from the database and renders them on the books page. The addBook function adds a new book to the database and redirects the user to the books page. If an error occurs, the function logs the error and sends a 500 status code response.

  • book.router.js
const express = require('express')
const { requiresAuth } = require('express-openid-connect')
const bookController = require('../controllers/book.controller')

const bookRouter = express.Router()

bookRouter.get('/', requiresAuth(), bookController.getAllBooks)
bookRouter.post('/', requiresAuth(), bookController.addBook)

module.exports = bookRouter

The routes for handling book requests. It uses the requiresAuth middleware from express-openid-connect to ensure that only authenticated users can access the books page. The routes are defined using the bookRouter object, which maps to the getAllBooks and addBook functions from the book.controller.js file. The bookRouter object is then exported.

  • index.js
const express = require("express")
const { requiresAuth } = require('express-openid-connect')
const path = require('path');
const mime = require('mime');

const auth0Middleware = require('./auth/auth0')
const CONFIG = require('./config/config')
const bookRouter = require("./routes/books.routes")
const connectToDb = require('./db/dbSetup')

const app = express()

// Connect to Mongodb Database
connectToDb()

app.use(express.json())
app.use(express.urlencoded({ extended: false }))

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'))

app.use(express.static(path.join(__dirname, 'views'), {
    setHeaders: (res, path) => {
      const contentType = mime.getType(path);
      res.set('Content-Type', contentType);
    },
}));

// auth router attaches /login, /logout, and /callback routes to the baseURL
app.use(auth0Middleware);

app.use('/books',  bookRouter)

app.get('/', (req, res) => {
    res.status(200).send(req.oidc.isAuthenticated() ? 'Logged in' : 'Logged out');
})


app.get('/profile', requiresAuth(), (req, res) => {
    const user = JSON.parse(JSON.stringify(req.oidc.user))
    res.render('profile', { user })
})

//Error handler middleware
app.use((err, req, res, next) => {
    console.error(err)
    const errorStatus = err.status || 500
    res.status(errorStatus).send(err.message)
    next()
})

app.listen(CONFIG.PORT, () => {
    console.info(`Server started on http://localhost:${CONFIG.PORT}`)
})

The index.js file is the entry point of the application, where the dependencies are imported and the server is started. It starts by importing the required modules such as express express-openid-connect path mime, and the custom modules such as auth0Middleware, CONFIG, bookRouter, and connectToDb.

The app variable is created by invoking the express function, which creates an express application. The app variable is then used to set the view engine to ejs, specify the views directory, and serve static files from the views directory.

The auth0Middleware middleware is added to the app, which handles authentication with Auth0. The bookRouter middleware is also added to the app with the "/books" route.

Two routes are defined on the app object. The first one is the home route "/", which returns either "Logged in" or "Logged out" depending on whether the user is authenticated or not. The second route is the profile route "/profile", which requires authentication and displays the user's profile information.

Finally, an error handler middleware is added to the app to handle any errors that may occur during the request/response cycle.

  • books.ejs
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="./style.css">
    <title>Bookstore</title>
</head>
<body>
    <h1>Welcome to the Bookstore!</h1>
    <h2>Add a Book</h2>
    <form action="/books" method="POST">
        <label for="title">Title:</label>
        <input type="text" id="title" name="title" required>
        <br>
        <label for="year">Year:</label>
        <input type="number" id="year" name="year" required>
        <br>
        <label for="price">Price:</label>
        <input type="number" id="price" name="price" required>
        <br>
        <button type="submit">Add Book</button>
    </form>

    <h2>All Books</h2>
    <% if (books.length > 0) { %>
        <table>
            <thead>
                <tr>
                    <th>Title</th>
                    <th>Year</th>
                    <th>Price</th>
                </tr>
            </thead>
            <tbody>
                <% books.forEach((book) => { %>
                    <tr>
                        <td><%= book.title %></td>
                        <td><%= book.year %></td>
                        <td><%= book.price %></td>
                    </tr>
                <% }) %>
            </tbody>
        </table>   
    <% } else { %> 
        <p>No Books Found</p>
    <% } %>
</body>
</html>

The books.ejs file is a view file that displays a form to add a book and a table that lists all the books. The form sends a POST request to the "/books" route. If there are no books to display, a message is displayed instead.

  • profile.ejs
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" type="text/css" href="./styles.css">
    <title>User Profile</title>
  </head>
  <body>
    <h1>User Profile</h1>
    <ul>
      <li><strong>Name:</strong> <%= user.name %></li>
      <li><strong>Nickname:</strong> <%= user.nickname %></li>
      <li><strong>Email:</strong> <%= user.email %></li>
      <li><strong>Email Verified:</strong> <%= user.email_verified %></li>
    </ul>
  </body>
</html>

we are displaying the user information on the /profile route by passing the user object to the profile.ejs view.

We have finished building our Auth0 login page, let's run the application by below command

node index.js

Our app is deployed on render that's why the url is different but its still the same process. Once our server is running on https://auth0-cln9.onrender.com/

Now, visit https://auth0-cln9.onrender.com/login route for authentication. Once you visit the route it redirects you to the Auth0 custom page for login.

Enter your details and you will be redirected back to the application

Since you logged in, now you can try to get your user details with the /profile endpoint and get and add books in the /books endpoint.

Conclusion

Auth0 has been successfully integrated into this codebase, providing robust authentication and authorization features for the Bookstore application. With the use of express-openid-connect, the application can implement secure login and logout functionality, as well as user profile management through the profile.ejs file. Overall, the implementation of Auth0 adds an extra layer of security and reliability, making it a trustworthy and efficient platform for managing users.

You can found the complete code used in this tutorial on our Github Repo

Link to deployed app on render

Did you find this article valuable?

Support Edet Asuquo by becoming a sponsor. Any amount is appreciated!