Home

Auth0

This guide steps through building a Next.js application with Auth0 and Supabase. We configure Auth0 to handle authenticating users and managing tokens, while writing our authorization logic in Supabase - using Row Level Security policies.

Note: This guide is heavily inspired by the Using Next.js and Auth0 with Supabase article on Auth0's blog. Check it out for a practical step-by-step guide on integrating Auth0 and Supabase.

The full code example for this guide can be found here.

Auth0 is an authentication and authorization platform, offering numerous strategies to authenticate and manage users. It provides fine-grain control over how users sign in to your application, the token that is generated, and what data is stored about your users.

Next.js is a web application framework built on top of React. We will be using it for this example, as it allows us to write server-side logic within our application. Auth0 have also written a very well integrated authentication library specifically for Next.js.

Note: API routes (serverless functions) in Next.js closely resemble the structure of Node server frameworks - such as Express, Koa and Fastify. The server-side logic in this guide could easily be refactored in one of these frameworks and managed as a separate application to the front-end.

If you don’t have an Auth0 account, create one here.

You will also need a Supabase account, which can be created by signing in here.

Step 1: Creating an Auth0 tenant#

From the Auth0 dashboard, click the menu to the right of the Auth0 logo, and select Create tenant.

Create tenant from Auth0 dashboard

Enter a Domain for your tenant - this will need to be unique.

Select a Region - this should be geographically close to the majority of your users.

Select Development for Environment Tag - this should be production when you're ready to go live.

Auth0 tenant settings

Step 2: Setting up an Auth0 application#

From the sidebar menu, select Applications > Applications and click Create Application.

Give your application a name, select the Regular Web Applications option and click Create.

Auth0 application settings

Select Settings and navigate to the Application URIs section, and update the following:

Allowed Callback URLs: http://localhost:3000/api/auth/callback

Allowed Logout URLs: http://localhost:3000

Scroll to the bottom of the Settings section and reveal the Advanced Settings.

Select OAuth and set JSON Web Token Signature to RS256.

Confirm OIDC Conformant is Enabled.

Click Save to update the settings.

Step 3: Creating a Supabase project#

From your Supabase dashboard, click New project.

Enter a Name for your Supabase project.

Enter a secure Database Password.

Select the same Region you selected for your Auth0 tenant.

Click Create new project.

New Supabase project settings

Step 4: Creating data in Supabase#

From the sidebar menu in the Supabase dashboard, click Table editor, then New table.

Enter todo as the Name field.

Select Enable Row Level Security (RLS).

Create two new columns:

  • title as text
  • user_id as text
  • is_complete as bool with the default value false

Click Save to create the new table.

Todo table

From the Table editor view, select the todo table and click Insert row.

Fill out the title field and click Save.

New row settings

Click Insert row and add a couple of extra todos.

List of todos

Step 5: Building a Next.js app#

Create a new Next.js project:

1npx create-next-app <name-of-project>

Create a .env.local file and enter the following values:

AUTH0_SECRET=any-secure-value
AUTH0_BASE_URL=http://localhost:3000
AUTH0_ISSUER_BASE_URL=https://<name-of-your-tenant>.<region-you-selected>.auth0.com
AUTH0_CLIENT_ID=get-from-auth0-dashboard
AUTH0_CLIENT_SECRET=get-from-auth0-dashboard
NEXT_PUBLIC_SUPABASE_URL=get-from-supabase-dashboard
NEXT_PUBLIC_SUPABASE_ANON_KEY=get-from-supabase-dashboard
SUPABASE_JWT_SECRET=get-from-supabase-dashboard

Note: Auth0 values can be found under Settings > Basic Information for your application.

Auth0 settings

Note: Supabase values can be found under Settings > API for your project.

Supabase settings

Restart your Next.js development server to read in the new values from .env.local.

1npm run dev

Step 6: Install Auth0 Next.js library#

Install the @auth0/nextjs-auth0 library.

1npm i @auth0/nextjs-auth0

Create a new file pages/api/auth/[...auth0].js and add:

// pages/api/auth/[...auth0].js

import { handleAuth } from '@auth0/nextjs-auth0'

export default handleAuth()

Note: This will create a few API routes for us. The main ones we will use are /api/auth/login and /api/auth/logout to handle signing users in and out.

Open pages/_app.js and wrap our Component with the UserProvider from Auth0:

// pages/_app.js

import React from 'react'
import { UserProvider } from '@auth0/nextjs-auth0/client'

const App = ({ Component, pageProps }) => {
  return (
    <UserProvider>
      <Component {...pageProps} />
    </UserProvider>
  )
}

export default App

Update pages/index.js to ensure the user is logged in to view the landing page.

// pages/index.js

import styles from '../styles/Home.module.css'
import { withPageAuthRequired } from '@auth0/nextjs-auth0'
import Link from 'next/link'

const Index = ({ user }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{' '}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
    </div>
  )
}

export const getServerSideProps = withPageAuthRequired()

export default Index

Note: withPageAuthRequired will automatically redirect the user to /api/auth/login if they are not currently logged in.

Test this is working by navigating to http://localhost:3000 which should redirect you to an Auth0 sign in screen.

Auth0 sign in screen

Either Sign up for a new account, or click Continue with Google to sign in.

You should now be able to view the landing page.

Landing page

Step 7: Sign Auth0 token for Supabase#

Currently, neither Supabase or Auth0 allow for a custom signing secret to be set for their JWT. They also use different signing algorithms.

Therefore, we need to extract the bits we need from Auth0's JWT, and sign our own to send to Supabase.

We can do that using Auth0's afterCallback function, which gets called anytime the user authenticates.

Install the jsonwebtoken library.

1npm i jsonwebtoken

Update pages/api/auth/[...auth0].js with the following:

// pages/api/auth/[...auth0].js

import { handleAuth, handleCallback } from '@auth0/nextjs-auth0'
import jwt from 'jsonwebtoken'

const afterCallback = async (req, res, session) => {
  const payload = {
    userId: session.user.sub,
    exp: Math.floor(Date.now() / 1000) + 60 * 60,
  }

  session.user.accessToken = jwt.sign(payload, process.env.SUPABASE_JWT_SECRET)

  return session
}

export default handleAuth({
  async callback(req, res) {
    try {
      await handleCallback(req, res, { afterCallback })
    } catch (error) {
      res.status(error.status || 500).end(error.message)
    }
  },
})

Our payload for the JWT will contain our user's unique identifier from Auth0 - session.user.sub and an expiry of 1 hour.

We are signing this JWT using Supabase's signing secret, so Supabase will be able to validate it is authentic and hasn't been tampered with in transit.

Note: We need to sign the user out and back in again to run the afterCallback function, and create our new token.

Now we just need to send the token along with the request to Supabase.

Step 8: Requesting data from Supabase#

Create a new file called utils/supabase.js and add the following:

// utils/supabase.js

import { createClient } from '@supabase/supabase-js'

const getSupabase = (access_token) => {
  const options = {}

  if (access_token) {
    options.global = {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    }
  }

  const supabase = createClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
    options
  )

  return supabase
}

export { getSupabase }

This will be our client for talking to Supabase. We can pass it an access_token and it will be attached to our request.

Let's load our todos from Supabase in our landing page!

// pages/index.js

import styles from '../styles/Home.module.css'
import { withPageAuthRequired } from '@auth0/nextjs-auth0'
import { getSupabase } from '../utils/supabase'
import Link from 'next/link'
import { useEffect } from 'react'

const Index = ({ user }) => {
  const [todos, setTodos] = useState([])
  const supabase = getSupabase(user.accessToken)

  useEffect(() => {
    const fetchTodos = async () => {
      const { data } = await supabase.from('todo').select('*')
      setTodos(data)
    }

    fetchTodos()
  }, [])

  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{' '}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  )
}

export const getServerSideProps = withPageAuthRequired()

export default Index

Alternatively, we could fetch todos on the server using the getServerSideProps function.

// pages/index.js

import styles from '../styles/Home.module.css'
import { withPageAuthRequired, getSession } from '@auth0/nextjs-auth0'
import { getSupabase } from '../utils/supabase'
import Link from 'next/link'

const Index = ({ user, todos }) => {
  return (
    <div className={styles.container}>
      <p>
        Welcome {user.name}!{' '}
        <Link href="/api/auth/logout">
          <a>Logout</a>
        </Link>
      </p>
      {todos?.length > 0 ? (
        todos.map((todo) => <p key={todo.id}>{todo.content}</p>)
      ) : (
        <p>You have completed all todos!</p>
      )}
    </div>
  )
}

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps({ req, res }) {
    const {
      user: { accessToken },
    } = await getSession(req, res)

    const supabase = getSupabase(accessToken)

    const { data: todos } = await supabase.from('todo').select('*')

    return {
      props: { todos },
    }
  },
})

export default Index

Either way, when we reload our application, we are still getting the empty state for todos.

Empty todo list

This is because we enabled Row Level Security, which blocks all requests by default. To enable our user to select their todos we need to write a policy.

Step 9: Write a policy to allow select#

Our policy will need to know who our currently logged in user is to determine whether or not they should have access. Let's create a PostgreSQL function to extract the current user from our new JWT.

Navigate back to the Supabase dashboard, select SQL from the sidebar menu, and click New query. This will create a new query called new sql snippet, which will allow us to run any SQL against our Postgres database.

Write the following and click Run.

create or replace function auth.user_id() returns text as $$
  select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text;
$$ language sql stable;

This will create a function called auth.user_id(), which will inspect the userId field of our JWT payload.

Note: To learn more about PostgreSQL functions, check out our deep dive video.

Let's create a policy that checks whether this user is the owner of the todo.

Select Authentication from the Supabase sidebar menu, click Policies, and then New Policy on the todo table.

Create new policy

From the modal, select Create a policy from scratch and add the following.

Policy settings for SELECT

This policy is calling the function we just created to get the currently logged in user's ID auth.user_id() and checking whether this matches the user_id column for the current todo. If it does, then it will allow the user to select it, otherwise it will continue to deny.

Click Review and then Save policy.

Note: To learn more about RLS and policies, check out our deep dive video.

The last thing we need to do is update the user_id columns for our existing todos.

Head back to the Supabase dashboard, and select Table editor from the sidebar.

User ID null in Supabase Table Editor

Each of our user_id columns are set to NULL!

To get the ID for our Auth0 user, head over to the Auth0 dashboard, select User Management from the sidebar, click Users and select your test user.

List of users in Auth0 dashboard

Copy their user_id.

User ID in Auth0 dashboard

Update each row in Supabase.

User ID set to Auth0 user

Now when we refresh our application, we should finally see our list of todos!

Note: Check out the repo for an example of writing new todos to Supabase.

Resources#