How to set up Auth0 and Neon Postgres in Next.js App Router

Femi-ige Muyiwa - Feb 13 - - Dev Community

Modern applications demand a balance of scalability, speed, and security to meet users’ ever-growing needs. Ensuring authentication and a robust database management system is crucial at the foundation of this triad. In this article, we’ll talk about the combination of Auth0 and Neon Postgres when you need to accommodate these two needs.

Auth0 is an identity platform that simplifies authentication and authorization functionalities across web, mobile, and native applications.

Neon is fully managed serverless Postgres that provides separate storage and computing to offer autoscaling, branching, and bottomless storage. Neon is open source under the Apache 2.0 licenses; you can check out the neondatabase GitHub repo.

This guide provides a comprehensive walk-through of setting up Auth0 with a Postgres database like Neon by building a login page that routes to a profile page while implementing Neon Postgres as the data store for user information. This application uses the Next.js React framework with App Router and Tailwind for styling.

Here is the link to the GitHub repository containing all the code

Prerequisites

To successfully complete this guide, you’ll need a few things:

Setting up a Neon Postgres project

After signing up, you’ll note Neon's streamlined project setup; you’ll just need to provide three things:

  • Project name
  • Postgres version
  • Database name

On the project setup page, there are more options to explore; for example, you can change the branch name to any other name, but we recommend leaving it as main for now. Next, click Create project.

Neon sign-in

Create project

More options

On the homepage, you'll find a popup containing all the connection details required to connect your application(s) to a Neon database. Copy these details to a safe file, such as a JSON file.

Connection details

Creating a Next.js application and installing dependencies

To create a Next.js application, open your terminal in any folder of your choice and type the command below:

npx create-next-app@latest <folder-name> --ts
Enter fullscreen mode Exit fullscreen mode

This command creates a Next.js application with TypeScript as the preferred programming language.

Next, we need to install some dependencies. These include an Auth0 dependency and dotenv to store sensitive credentials in a .env file. To do this, run the command below:

npm install @auth0/nextjs-auth0 dotenv
Enter fullscreen mode Exit fullscreen mode

Configuring Auth0 application settings

After signing into the Auth0 account you created earlier, click Create Application in the Auth0 dashboard, select Regular Web Applications, and fill in the app name to create traditional web applications that use redirects.

dashboard

Regular web application

The following prompt asks what technology is being used for the project; we’ll select Next.js out of all the presented options for this project.

Technology

Over at your just-created Regular Web Applications, you’ll need to configure the URLs for your app under the application URI. To do this, head to Settings > Allowed Callback URLs and type in the URL below within the input field:

http://localhost:3000/api/auth/callback
Enter fullscreen mode Exit fullscreen mode

Here, we use port 3000 as the port our application runs on, so make sure to select the port your application is running on.

Note: you can add multiple URLs by separating them with a comma ","

The URL above completes the web application's authentication flow by triggering the callback route after successful authentication. The callback route api/auth/callback is integrated by Auth0 into the @auth0/nextjs-auth0 package installed earlier, which will handle the callback flow.

Next, we’ll add the following URL in the Allowed Logout URLs input field:

http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

Setting this allows a redirect back to our web application after logging out, and you can use a personal URL if your project requires it.

Allowed callbacks

Once you're done with that, save the changes and head to the top page of the Settings section. Make sure to note the Domain, Client ID, and Secret, as they will come in handy in the coming sections.

Basic information

Connecting NextJs to Auth0

To initiate a connection between Auth0 and Next.js, head back to the Next.js application we created and create a .env.local file in the root folder to store the Domain, Client ID, and Secret from Auth0. Then, paste the following variables into the file below:

AUTH0_SECRET= use [openssl rand -hex 32] to generate a 32 bytes value
AUTH0_BASE_URL= http://localhost:3000
AUTH0_ISSUER_BASE_URL= https://AUTH0-DOMAIN
AUTH0_CLIENT_ID= AUTH0-CLIENT-ID
AUTH0_CLIENT_SECRET= AUTH0-CLIENT-SECRET
Enter fullscreen mode Exit fullscreen mode

Note: You can get these pages from Auth0's Nextjs documentation and replace the values with those of your Auth0 regular web app's values.mk

We will need to generate a 32-byte random value for the AUTH0_SECRET, and this can be done using Node's crypto module. The crypto module generates an arbitrary hexadecimal string of length 32. Thus, head to your terminal and type the command below to use the module:

node -e "console.log(crypto.randomBytes(32).toString('hex'))"
Enter fullscreen mode Exit fullscreen mode

Creating an API route for Auth0

Next, we'll need to add the dynamic API route; we’ll do so by creating a /api directory within the /app directory. Within the now /app/api directory, create an /auth directory to handle all the authentication-related things within this project (create this directory instead if you already have an /api).

Within the /auth directory, create a new unique directory called /[auth0] and create a file route.tsx within the folder. Using a directory in this manner, /[auth0] indicates that the directory name auth is a dynamic segment.

In the route.tsx file, we will use the handleAuth method from the @auth0/nextjs-auth0 to handle the authentication flow of our web application. The handleAuth creates multiple route handlers under the hood — this means that when we log initially into Auth0, the routes followed are api-auth-login. So, the handleAuth function will handle all the login flow based on that and log out the callback URLs set earlier.

So, let’s add the code below to the route.tsx to implement what we just explained above:

import { handleAuth } from "@auth0/nextjs-auth0";
export const GET = handleAuth();
Enter fullscreen mode Exit fullscreen mode

Next, we need to wrap our application in an Auth0 UserProvider. In the layout.tsx file from the base /app directory, we’ll import UserProvider from @auth0/nextjs-auth0/client, and then we'll wrap the body tag in a UserProvider.

So here is the updated layout.tsx file below:

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { UserProvider } from "@auth0/nextjs-auth0/client";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
  title: "Auth0 and Neon Postgres",
  description: "Generated by create next app",
};
export default function RootLayout({
  children,
}: {
  children: React.ReactNode,
}) {
  return (
    <html lang="en">
      <UserProvider>
        <body className={inter.className}>{children}</body>
      </UserProvider>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

We perform this operation using an Auth0 platform to create and authenticate users. Therefore, we will have to wrap our entire application in Auth0. At the moment, these are all the configurations needed for our web application. In the coming sections, we will create pages for logging in and a profile page to display certain user information.

Building the pages and routes using Next.js and Auth0

This project will use the initial page (/app/page.tsx) and profile routes. The initial page route will be a form with email, password input, and a submit button. Also, there will be a button that triggers a connection to Auth0. For the profile area, a simple card will show the user's profile image, name, and email. The card will also contain a logout button that triggers a logout action from Auth0.

The idea is to give a clearer picture of how the App Router operates after successful authentication with Auth0. Earlier, we enabled App Router when we created our project — thus, route by creating another folder in the /app directory, let's call it profile. Within this profile directory, create a file called page.tsx. With that, you've successfully created a route with App Router.

Head back to the /app/page.tsx file, clear out the initial template, and replace it with the one below:

export default function Login() {
  return (
    <main className="flex min-h-screen items-center justify-center">
      <div className="w-[300px] md:w-[600px] shadow-lg p-5">
        <p className="text-5xl">Login</p>
        <div className="h-[20px]" />
        <form>
          <input
            type="email"
            placeholder="E-mail"
            className="border-2 border-black w-full p-5 text-2xl rounded-lg focus:outline-blue-400"
            name="email"
          />
          <div className="h-[20px]" />
          <input
            type="password"
            placeholder="Password"
            className="border-2 border-black w-full p-5 text-2xl rounded-lg focus:outline-blue-400"
            name="password"
          />
          <div className="h-[40px]" />
          <div className="flex justify-end">
            <button className="border-2 border-black p-5 px-10 text-2xl rounded-lg">
              Try Me
            </button>
          </div>
        </form>
        <div className="h-[20px]" />
        <p className="flex justify-center font-bold text-2xl">Or</p>
        <div className="h-[20px]" />
        <button className="border-2 w-full border-black p-5 px-10 text-2xl rounded-lg">
          Sign in with Auth0
        </button>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

The code above is the login UI template mentioned earlier, and when we run the code, our application will look like the image below:

Login template

Still within the /app/pages.tsx file, import useUser function from @auth0/nextjs-auth0/client. Then return user, error, isLoading from the useUser function in the Login function. The user object allows us to display user information after a successful login. In this case, where we experience any errors during signups or login, the error object will display the error message. isLoading is a Boolean parameter that lets us know when data is being loaded.

Also, we will use Next.js navigation to handle navigation using the useRouter function. To do this, we’ll import the useRouter function from next/navigation, then return push.

With this, we can use a condition to check if there is a user object, and if it is true, push it to the /profile route created earlier. Also, we will use the useRouter function to create a login link that will allow us to sign up and log in to our Auth0 application.

First, create a function that pushes to the route api/auth/login and set an onClick event to the function in the Sign in with Auth0 button from our template code.

Below is the updated code for the page.tsx:

"use client";
import { useRouter } from "next/navigation";
import { useUser } from "@auth0/nextjs-auth0/client";
export default function Login() {
  const { push } = useRouter();
  const { user, error, isLoading } = useUser();
  if (user) {
    push("/profile");
  }
  if (isLoading) {
    return <div>...loading</div>;
  }
  if (error) {
    return <div>{error.message}</div>;
  }
  const handleLogin = () => push("api/auth/login");
  return (
    <main className="flex min-h-screen items-center justify-center">
      <div className="w-[300px] md:w-[600px] shadow-lg p-5">
        <p className="text-5xl">Login</p>
        <div className="h-[20px]" />
        <form>
          <input
            type="email"
            placeholder="E-mail"
            className="border-2 border-black w-full p-5 text-2xl rounded-lg focus:outline-blue-400"
            name="email"
          />
          <div className="h-[20px]" />
          <input
            type="password"
            placeholder="Password"
            className="border-2 border-black w-full p-5 text-2xl rounded-lg focus:outline-blue-400"
            name="password"
          />
          <div className="h-[40px]" />
          <div className="flex justify-end">
            <button className="border-2 border-black p-5 px-10 text-2xl rounded-lg">
              Try Me
            </button>
          </div>
        </form>
        <div className="h-[20px]" />
        <p className="flex justify-center font-bold text-2xl">Or</p>
        <div className="h-[20px]" />
        <button
          onClick={handleLogin}
          className="border-2 w-full border-black p-5 px-10 text-2xl rounded-lg"
        >
          Sign in with Auth0
        </button>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

When you click the button above, an Auth0 login will be created successfully, but the objective is incomplete as there is currently nothing to display in the /profile/page.tsx. So, we’ll add a UI template code that contains a user image area, name, email, and a logout button.

Thus, head to the /profile/page.tsx file and paste the template code below to it:

export default function Profile() {
  return (
    <main className="max-w-[1900px] flex flex-col mx-auto mt-5 rounded-2xl overflow-hidden">
      <div className="h-[250px] bg-black relative">
        <div className="h-[150px] w-[150px] rounded-full border-2 bg-gray-600 absolute -bottom-[75px] z-50 left-10 overflow-hidden">
          <img
            src={user?.picture ?? "default-image-url"}
            alt=""
            className="object-cover h-full, w-full"
          />
        </div>
      </div>
      <div className="h-[300px] relative transl bg-slate-600 p-5 flex justify-between">
        <div>
          <p className="mt-[90px] text-3xl font-bold">
            {user?.name ?? "default-name"}
          </p>
          <p className="mt-[20px] text-2xl">{user?.email ?? "default-email"}</p>
        </div>
        <div>
          <button className="border-2 border-black p-5 px-10 text-2xl rounded-lg cursor-pointer">
            Log Out
          </button>
        </div>
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

When we run the code, the /profile route should look like the image below:

Profile template

Still within the /app/profile/page.tsx file, we'll do similarly to what we did in the /app/pages.tsx file with a slight twist. Start by importing the useUser function and the withPageAuthRequired function from @auth0/nextjs-auth0/client. Then return user, error, isLoading from the useUser function in the Profile function.

Next, use the useRouter function to create a logout link to allow us to log out of our Auth0 application. To do this, create a function that pushes to the route api/auth/logout and set an onClick event to the function in the Log Out button from the /profile route template code.

The withPageAuthRequired is a higher-order component used to protect pages requiring authentication. So, we will need to wrap the Profile function with a map object that contains properties onRedirecting and onError that shows specific messages per the context.

Here is the updated code to the /app/profile/page.tsx file:

"use client";
import { useUser, withPageAuthRequired } from "@auth0/nextjs-auth0/client";
import { useRouter } from "next/navigation";
function Profile() {
  const { user, error, isLoading } = useUser();
  const { push } = useRouter();
  // arrow function that routes to the logout api from the [handleAuth] method
  const handleLogout = () => push("api/auth/logout");
  // handle isLoading state from Auth0
  if (isLoading) {
    return <div>...loading</div>;
  }
  // handle error state from Auth0
  if (error) {
    return <div>{error.message}</div>;
  }
  console.log(user?.email);

  // main profile pagelayout
  return (
    <main className="max-w-[1900px] flex flex-col mx-auto mt-5 rounded-2xl overflow-hidden">
      <div className="h-[250px] bg-black relative">
        <div className="h-[150px] w-[150px] rounded-full border-2 bg-gray-600 absolute -bottom-[75px] z-50 left-10 overflow-hidden">
          <img
            src={user?.picture ?? "default-image-url"}
            alt=""
            className="object-cover h-full, w-full"
          />
        </div>
      </div>
      <div className="h-[300px] relative transl bg-slate-600 p-5 flex justify-between">
        <div>
          <p className="mt-[90px] text-3xl font-bold">{user?.name}</p>
          <p className="mt-[20px] text-2xl">{user?.email}</p>
        </div>
        <div>
          <button
            onClick={handleLogout}
            className="border-2 border-black p-5 px-10 text-2xl rounded-lg cursor-pointer"
          >
            Log Out
          </button>
        </div>
      </div>
    </main>
  );
}
export default withPageAuthRequired(Profile, {
  onRedirecting: () => <div>Redirecting to Login ....</div>,
  onError: (error) => <div>{error.message}</div>,
});
Enter fullscreen mode Exit fullscreen mode

With this, we have reached a milestone, as when we run our application with the command npm run dev, we can authenticate our application using Auth0.

auth0 next js | Opentape

Muyiwa Femi-Ige - Feb 13th, 9:26am

favicon app.opentape.io

You’re likely wondering where this data is stored after logging in or signing up. Auth0 utilizes its database to store the data, but we'd like to change that by implementing Auth0 with a custom database — in this case, Neon Postgres. This process ensures user data is available in the primary data store (Neon) for other application development purposes.

The next section demonstrates how to use Neon as a custom database and sets up some database action scripts for direct communication between Auth0 and Neon Postgres. Let’s jump in!

Setting up Neon as a custom database on Auth0

Head back to your Auth0 dashboard, and from there, select Authentication → Database.

database

In the Database section, create a database connection by clicking Create DB Connection, fill in your desired Connection name, and click Create.

database connection

database connection name

In your newly created connection, head to the Custom Database section, where all the actions will take place. Toggle the Use my own database option and scroll to the Database Action Scripts area.

custom database

DAS

In the Database Action Scripts section, we can create custom serverless scripts that perform specific actions on our database. The Login script is mandatory; you can set the others as you please. For this tutorial, we will create templates for the six scripts in the image above, and here is their code below in succession:

Login script

This script is executed during each login to validate the user's authenticity.

function login(email, password, callback) {
  const { Pool } = require("pg");
  const bcrypt = require("bcrypt");

  const pool = new Pool({
    connectionString: configuration.POSTGRES_URL,
  });

  const query =
    "SELECT id, nickname, email, password FROM users WHERE email = $1";
  pool.query(query, [email], function (err, result) {
    if (err || result.rows.length === 0)
      return callback(err || new WrongUsernameOrPasswordError(email));

    const user = result.rows[0];

    bcrypt.compare(password, user.password, function (err, isValid) {
      if (err || !isValid)
        return callback(err || new WrongUsernameOrPasswordError(email));

      return callback(null, {
        user_id: user.id,
        nickname: user.nickname,
        email: user.email,
      });
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Create script

This script is executed after a user signs up. A database table row is created for the user with the information filled in during the signup process.

function create(user, callback) {
  const { Pool } = require("pg");
  const bcrypt = require("bcrypt");

  const pool = new Pool({
    connectionString: configuration.POSTGRES_URL,
  });

  bcrypt.hash(user.password, 10, function (err, hashedPassword) {
    if (err) return callback(err);

    const query =
      "INSERT INTO users(nickname, email, password) VALUES ($1, $2, $3)";
    pool.query(
      query,
      [user.username, user.email, hashedPassword],
      function (err, result) {
        if (err) return callback(err);

        return callback(null);
      }
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

Verify script

This script verifies the email verification status of a user.

function verify(email, callback) {
  const { Pool } = require("pg");
  const pool = new Pool({
    connectionString: configuration.POSTGRES_URL,
  });

  const query =
    "UPDATE users SET email_verified = true WHERE email_verified = false AND email = $1";
  pool.query(query, [email], function (err, result) {
    if (err) return callback(err);

    return callback(null, result && result.rowCount > 0);
  });
}
Enter fullscreen mode Exit fullscreen mode

Change password script

This script changes the user's encrypted password on the database after following the change password during a password reset process.

function changePassword(email, newPassword, callback) {
  const { Pool } = require("pg");
  const bcrypt = require("bcrypt");

  const pool = new Pool({
    connectionString: configuration.POSTGRES_URL,
  });

  bcrypt.hash(newPassword, 10, function (err, hash) {
    if (err) return callback(err);

    const query = "UPDATE users SET password = $1 WHERE email = $2";
    pool.query(query, [hash, email], function (err, result) {
      if (err) return callback(err);

      return callback(err, result && result.rowCount > 0);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Get user script

This script tests if a user exists in a database. For example, a scenario in which the user wants to change their password.

function loginByEmail(email, callback) {
  const { Pool } = require("pg");

  const pool = new Pool({
    connectionString: configuration.POSTGRES_URL,
  });

  const query = "SELECT id, nickname, email FROM users WHERE email = $1";
  pool.query(query, [email], function (err, result) {
    if (err || result.rows.length === 0) return callback(err);

    const user = result.rows[0];

    return callback(null, {
      user_id: user.id,
      nickname: user.nickname,
      email: user.email,
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

Delete script

This Script is activated when the user is deleted from Auth0 — thus, the user data is subsequently removed from the database.

function remove(id, callback) {
  const { Pool } = require("pg");

  const pool = new Pool({
    connectionString: configuration.POSTGRES_URL,
  });

  const query = "DELETE FROM users WHERE id = $1";
  pool.query(query, [id], function (err) {
    if (err) return callback(err);

    return callback(null);
  });
}
Enter fullscreen mode Exit fullscreen mode

Note: With Neon, you can manage your database using a few options, such as a Command Line Interface (CLI), the Neon API, or a Structured Query Language (SQL) tool.

After updating the Database Action Scripts, update your Database settings with some sensitive information you used to validate the scripts. For this tutorial, we used the Neon Postgres pooled connection URL.

To get this URL, head back to your Neon dashboard and copy out the Connection String from the connection detail area. Ensure to mark Pooled Connection as true before copying the details.

connection detail

Back in Auth0's Database settings area, create a key with the name POSTGRES_URL and value with the Pooled Connection string copied from Neon Postgres.

Database settings

Still within your Auth0 Database Connection, head to Applications and toggle on the Regular Web Applications. We have successfully set Neon Postgres as a custom database for our Auth0 application.

applications

Creating users database table on Neon

With the project all but done, there is one tiny thing to add: creating the table for the user store. When creating the Database Action Script, we specified that we are querying a table called users, so let’s make that our table name.

For our table, we will create five fields: id, nickname, email, password, and email_verified. So, head to the SQL Editor in Neon's console and run the query below:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users(
    id UUID DEFAULT uuid_generate_v4(),
    nickname VARCHAR(255),
    email VARCHAR(255) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    email_Verified BOOLEAN DEFAULT FALSE
);
Enter fullscreen mode Exit fullscreen mode

The query above creates a UUID column with a default value generated by uuid_generate_v4() function. It is followed by creating a nickname column — a variable character (varchar) column with a maximum length of 255 characters.

Also, it creates an email and password column, a non-null varchar column with a maximum length of 255 characters, and a unique constraint.

Finally, it creates an email_Verified column, a Boolean column with a default value of false.

When we initialize a signup flow with Auth0, here’s the result on the Neon Postgres database table:

Neon table

Conclusion

This tutorial taught us how to leverage security and database management by integrating Auth0 with Neon Postgres as a custom database. Pairing Auth0 and Neon Postgres represents a pivotal leap toward fortifying modern applications with scalability, speed, and security. By adopting this strategic combination, developers can ensure a seamless and secure authentication process through Auth0 while leveraging the robust database management capabilities of Neon Postgres.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .