DEV Community

Cover image for Building Secure Neon-Infused Web Apps with Auth0, Express, and EJS
teri
teri

Posted on

Building Secure Neon-Infused Web Apps with Auth0, Express, and EJS

Data storage management is required for modern web applications to operate optimally and scale automatically. Without data storage management, your current session on social media apps won't remember or display your most recent messages on the platform.

This service is needed to prevent data leakage and unauthorized access to user information. It is important to note that the setup and configuration of Auth0 will act as a secure authentication system.

Neon is a cloud-native serverless Postgres database that lets you quickly spin up a database instance and connect with it. It has features like autoscaling, branching, and bottomless storage, with its architecture supporting separate computing and storage.

This article examines how to set up and leverage the capabilities of Auth0 with a Postgres database like Neon for any development workflow that needs storage and user authentication.

Prerequisites

You should consider the following to complete the tutorial:

  • Install Node.js >= 20 on your local machine
  • Familiarity with using Embedded JavaScript (EJS)
  • An Auth0 account for creating an application with authentication

Check out the source code and the live app for a complete review of this build.

Understanding Auth0

Auth0 is a cloud-based user authentication, authorization, management, and security platform that serves as the gateway access to your application.

It enables developers to add secure authentication and authorization flows to applications (web and mobile) with tools and services that include features like universal login, multifactor authentication (MFA), and more.

Connecting to a Neon Database

Since Neon is a cloud-based service, you need an account. Sign up for a free tier now!

Once you log in as a new user, Neon prompts you to create a new project. Fill in the required parameters and click the Create project button to create a database.

setup screen

After creating the database, the dashboard shows the connection string with its details, as seen below; select the database and the type of connection.

Note: In Neon, databases are stored on branches. By default, a project has one branch and one database.
You can select the branch and database from the available drop-down menus.

connection string

Let’s create a database table from the Neon console. Navigate to the menu's left pane and click on the SQL Editor. The function of the SQL Editor is to run queries on your Neon databases.

The following SQL creates a table called student and inserts some values into it:

    CREATE TABLE student (
    student_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    first_name VARCHAR(255) NOT NULL,
    last_name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    );

    INSERT INTO student (first_name, last_name, email)
    VALUES ('Teri', 'Eyenike', 'oteri@iamteri.tech'),
    ('Dubem', 'Ochuaka', 'chidumebi@gmail.com'),
    ('Kola', 'Odugbesan', 'kolapo@gmail.com'),
    ('Gojep', 'Kwachu', 'gojep@yahoo.com'),
    ('Chinasa', 'Muoneke', 'chinasa@hotmail.com');
Enter fullscreen mode Exit fullscreen mode

Running the above CREATE command may raise an error if the UUID extension, which generates unique identifiers for primary keys, is not installed.

Run this command below, and afterward, rerun the create and insert command:

    CREATE extension IF NOT EXISTS "uuid-ossp"
Enter fullscreen mode Exit fullscreen mode

In the CREATE and INSERT codes above, the following is happening:

  • Create: The Create command creates a new table called student to store the student information, and within it are the defined data types for each domain/constraint
  • Insert: The Insert command adds the data into the student table, specifying each attribute or field accordingly

Remember to query the student table with this command:

    SELECT * FROM student;
Enter fullscreen mode Exit fullscreen mode

Click on the Tables menus to see the data arranged in rows and columns as shown below:

student table

Connecting Postgres Locally

There are two ways to connect with the Postgres database which can be in the terminal or using a Postgres GUI client app like DBeaver. But first, download the PostgreSQL installer for macOS or Windows, depending on your OS. The setup and installation come with the psql command, a tool shipped with Postgres that allows you to communicate with Postgres through the command line.

Remember to add Postgres to your PATH variable by following the instructions in the documentation.

Head over to your Neon console dashboard and copy the connection string details. Paste the string in your terminal to connect to your Neon Postgres database. Here’s an example:

    psql 'postgresql://Terieyenike:Gxxx@ep-odd-butterfly-33244281.eu-central-1.aws.neon.tech/neondb?sslmode=require'
Enter fullscreen mode Exit fullscreen mode

To query and select the entire student table in the terminal, run the same command as in the SQL editor in your dashboard:

    select * from student;
Enter fullscreen mode Exit fullscreen mode

You should see something like this:

query result from student table

You can repeat the same process for inserting user data into the student table in neon using the INSERT INTO command in the terminal

Setting up Auth0

Log in to your admin Auth0 dashboard to create a new application. An application in Auth0 can be a native app that executes on a mobile device, a single-page web app that runs on a browser, or a regular web application that executes on a web server. The latter is chosen for development using the Express framework in this tutorial.

To create a new application, navigate to the Applications menu and select Applications from the dropdown.

creating applications in auth0

After that, click the + Create Application button which opens a dialog box to choose an application type. Make sure you name your application and select the Regular Web Applications.

regualar web applications

The most crucial step of this setup is choosing a technology for the project. Select Node.js (Express).

web technology

Installation and setup

Make a new directory in the terminal by running:

    mkdir neon-nodejs
Enter fullscreen mode Exit fullscreen mode

Navigate into the directory and initialized it with the command:

cd neon-nodejs

npm init -y
Enter fullscreen mode Exit fullscreen mode

The npm init with the flag -y accepts all the defaults and creates the package.json file which contains metadata for the application and the dependencies for the app's functioning.

Install these dependencies:

npm install dotenv ejs express express-openid-connect pg
Enter fullscreen mode Exit fullscreen mode

The command above takes care of the following:

  • dotenv: loads environment variables (key-value pairs) from the .env file, keeping them secrets
  • ejs: a templating language that generates HTML markup with plain JavaScript in Node.js
  • express: backend framework for creating and building APIs
  • express-openid-connect: It provides an identity layer to verify the identity of users
  • pg: the package for interfacing with the PostgreSQL database and connecting to the Neon database

Also, install nodemon, which will listen to changes and restart the server:

npm install --save-dev nodemon
Enter fullscreen mode Exit fullscreen mode

Update and replace the code to the package.json in the scripts section:

    {
      "scripts": {
        "start": "node app.js",
        "dev": "nodemon app.js"
      },
    }
Enter fullscreen mode Exit fullscreen mode

Create the application entry point with the command which creates a new file in the directory, neon-nodejs:

touch app.js
Enter fullscreen mode Exit fullscreen mode

To run the application, use:

    npm run dev
Enter fullscreen mode Exit fullscreen mode

Integrating Auth0 in Node.js

Click the Integrate Now button, and the following screen accepts the defaults to configure Auth0.

You can change it if you are using a different port number from 3000. In your entry app file, app.js, the server will function on port 3000.

integration

Creating Environment Variables
After clicking on the Save Settings And Continue button from above, on the next screen under the Configure Router, replace the values for secret, baseURL, clientID, and issuerBaseURL and replace them with yours in the .env file:

DATABASE_URL="Replace with your database url connection string in NeonDB"
SECRET="generate a secret key with the command openssl rand -hex 32"
BASE_URL=http://localhost:3000
CLIENT_ID="Replace with your Auth0 client id"
ISSUER_BASE_URL="Replace with your Auth0 issuer base url"
Enter fullscreen mode Exit fullscreen mode

Note: To generate a SECRET key, run this command openssl rand -hex 32 in your terminal and replace its value as given

Let’s jump in with creating the server.

In the entry file, add this code block:

    // app.js

    const path = require('path');
    const express = require('express');
    const { auth } = require('express-openid-connect');
    const { requiresAuth } = require('express-openid-connect');
    const { Pool } = require('pg');

    const app = express();

    require('dotenv').config();

    const config = {
      authRequired: false,
      auth0Logout: true,
      secret: process.env.SECRET,
      baseURL: process.env.BASE_URL,
      clientID: process.env.CLIENT_ID,
      issuerBaseURL: process.env.ISSUER_BASE_URL,
    };

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

    app.use(express.json());
    app.use("/", express.static(path.join(__dirname, "public")));
    app.use(auth(config));

    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
      ssl: {
        require: true,
      },
    });


    app.get("/", async (req, res) => {
      const client = await pool.connect();
      const response = await client.query('SELECT * FROM student');
      res.render("index", {
        title: "Neon authentication with auth0",
        students: response.rows,
        isAuthenticated: req.oidc.isAuthenticated(),
        user: req.oidc.user,
      });
    })

    app.get("/profile", requiresAuth(), (req, res) => {
      res.render("profile", {
        title: "Secured profile page",
        isAuthenticated: req.oidc.isAuthenticated(),
        user: req.oidc.user,
      });
    })

    app.listen(process.env.PORT || 3000, () => {
      console.log('Server is running on port 3000');
    })
Enter fullscreen mode Exit fullscreen mode

This code does the following:

  • path: The path import is responsible for working with directories and file paths
  • The other imports for the app
  • const app = express(): It creates a new instance calling the express function on the app variable
  • require('dotenv').config(): this function will read the sensitive information from the .env file, which parses the content and assigns it to process.env
  • const config: objects configure the auth0 router the Express OpenID Connect library
  • app.set: sets the view engine as ejs, which is responsible for rendering the page as a web page, and the views is the folder that contains the rendered pages for each page route
  • app.use: the middlewares have access request (req) and response (res) object
  • app.get("/"): This is the route handler for the root URL (“/”). Within the callback function is the database query from Neon DB connected using the student table and sends a response to the rendered HTML index.ejs to the client
  • The other GET method with the endpoint, /profile is a secured page using the requiresAuth middleware which is available only when there is a valid user session
  • The app listens on port 3000 with the app.listen function

Displaying the View

Let’s create the following directories within the root of the app. For the stylesheets, create a public folder and within it, a style.css file in a stylesheets folder, which should look like this:

├── public
│   └── stylesheets
│       └── style.css
Enter fullscreen mode Exit fullscreen mode

Copy-paste this style in the style.css file:

    //  public/stylesheets/style.css

    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
      border: 0;
    }

    body {
      font-family: 'Inter', sans-serif;
      font-size: 16px;
      line-height: 1.5;
      color: #333;
      background-color: #fff;;
    }

    nav {
      padding: 2em 0;
    }

    nav li + li {
      margin-top: 1em;
    }

    ul {
      list-style: none;
      margin: 0;
      padding: 0;
    }

    main {
      min-height: 100vh;
      object-fit: contain;
      width: 80%;
      margin: 0 auto;
      max-width: 75rem;
    }

    .info {
      padding-top: 2em;
      padding-bottom: 2em;
    }

    .info h3 {
      margin-bottom: 1.5em;
    }

    a {
      text-decoration: none;
      color: #333;
      transition: all 0.3s ease-in;
    }

    a:hover {
      color: #3C6C84
    }

    img {
      max-width: 100%;
    }

    .student-info {
      display: grid;
       grid-template-columns: repeat(auto-fit, minmax(19rem, 1fr));
      gap: 2rem;
      padding: 2em 0;

    }

    .student-info ul {
      background: #FBEED3;
      padding: 1em 2em;
      overflow: hidden;
      user-select: none;
    }

    .student-info ul li {
      font-weight: 700;
      margin-bottom: .75em;
    }

    .profileDetails img {
      border-radius: 50%;
      height: 100px;
      width: 100px;
    }

    .profileDetails p {
      margin-bottom: 1em;
    }

    pre {
      white-space: auto;
    }

    footer {
      background-color: #333;
      color: white;
      padding: 1em 2em;
      width: 100%;
    }
Enter fullscreen mode Exit fullscreen mode

Now, for the views, create a folder structure like this:

views
    ├── index.ejs
    ├── partials
    │   ├── footer.ejs
    │   ├── header.ejs
    │   └── nav.ejs
    └── profile.ejs
Enter fullscreen mode Exit fullscreen mode

The files in the partials folder are reusable code that does not change across the web pages, meaning they are static and included across multiple views or templates using the tag <%- include(partials/<name-of-file>); -%>.

Copy-paste the following:

    //  views/partials/footer.ejs

    <footer>Teri &copy; <%= new Date().getFullYear() %>
    </footer>
Enter fullscreen mode Exit fullscreen mode

The output value of EJS templates is wrapped tags, which vary depending on the use case.

    //  views/partials/header.ejs

    <!DOCTYPE html>
    <html lang="en">

    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Secure routes with auth0</title>
      <link rel="stylesheet" href="stylesheets/style.css">
    </head>

    <body>
Enter fullscreen mode Exit fullscreen mode
    //  views/partials/nav.ejs

    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/profile">Profile</a></li>
        <% if (!isAuthenticated) { %>
          <li><a href="/login">Login</a></li>
          <% } else { %>
            <li>
              <a href="/logout">Logout</a>
            </li>
            <% } %>
      </ul>
    </nav>
Enter fullscreen mode Exit fullscreen mode

The code above, using conditionals, checks when an authenticated user is logged in and displays the right page and navigation menu.

    //  views/index.ejs

    <%- include('partials/header'); -%>
      <main>
        <%- include('partials/nav'); -%>
          <div class=" info">
            <% if (user) { %>
              <h3>
                Welcome, <%= user.name %>
              </h3>

              <div class=" student-info">
                <% students.forEach(function(student){ %>
                  <ul>
                    <li>
                      <%= student.first_name %>
                        <%= student.last_name %>
                    </li>
                    <p>
                      <%= student.email %>
                    </p>
                  </ul>
                  <% }) %>
              </div>
              <% } else { %>
                <h1>
                  <%= title %>
                </h1>
                <p>You need to login to view the page details</p>
                <% } %>
          </div>
      </main>
      <%- include('partials/footer'); -%>
Enter fullscreen mode Exit fullscreen mode

This page will display the rendered information from the saved data in Neon in a grid format as well as welcome the user with their name as below:

student data with their name and email addresses

    //  views/profile.ejs

    <%- include('partials/header') -%>
      <main>
        <%- include('partials/nav') -%>
          <div class="info">
            <h2>
              <%= title %>
            </h2>
            <% if (user) { %>
              <div class="profileDetails">
                <p>
                  Hello, <%= user.name %>
                </p>


                <pre><code><%= JSON.stringify(user,null,2) %></code></pre>
              </div>
              <% } %>
          </div>
      </main>
Enter fullscreen mode Exit fullscreen mode

The code above includes the header and navigation menu that displays the user information as an object.

auth0 secured profile page

neon with auth0 and ejs - Watch Video

demo app

Final Thoughts

In this article, you learned how to set up and connect a PostgreSQL database using Neon and Auth0 to protect and secure web pages, giving access to only logged-in users.

source code

Live app

Resources

Top comments (0)