Files
supabase/apps/reference/docs/guides/integrations/auth0.mdx
2022-08-13 22:24:54 +08:00

462 lines
15 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: auth0
title: 'Auth0'
description: 'Swap out Supabase authentication with Auth0. Let Auth0 handle tokens and signing users in and out, while Supabase enforces authorization policies with Row Level Security (RLS).'
---
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](https://auth0.com/blog/using-nextjs-and-auth0-with-supabase/) article on [Auth0's blog](https://auth0.com/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](https://github.com/dijonmusters/supabase-auth0-example).
[Auth0](https://auth0.com/) 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](https://nextjs.org/) 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](https://www.npmjs.com/package/@auth0/nextjs-auth0) 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 dont have an Auth0 account, create one [here](https://auth0.com/signup).
You will also need a Supabase account, which can be created by signing in [here](https://app.supabase.com/).
## 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](/img/guides/integrations/auth0/IYzHxeW.png)
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](/img/guides/integrations/auth0/iSA3E0J.png)
## 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](/img/guides/integrations/auth0/ANU4Wez.png)
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](https://app.supabase.com/), 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](/img/guides/integrations/auth0/qnmJEU7.png)
## Step 4: Creating data in Supabase
From the sidebar menu in the [Supabase dashboard](https://app.supabase.com/), 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](/img/guides/integrations/auth0/33kqP4K.png)
From the `Table editor` view, select the `todo` table and click `Insert row`.
Fill out the `title` field and click `Save`.
![New row settings](/img/guides/integrations/auth0/mEhHAWC.png)
Click `Insert row` and add a couple of extra todos.
![List of todos](/img/guides/integrations/auth0/dLOvhdq.png)
## Step 5: Building a Next.js app
Create a new Next.js project:
```bash
npx 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_KEY=get-from-supabase-dashboard
SUPABASE_SIGNING_SECRET=get-from-supabase-dashboard
```
> Note: Auth0 values can be found under `Settings > Basic Information` for your application.
![Auth0 settings](/img/guides/integrations/auth0/o07FaoV.png)
> Note: Supabase values can be found under `Settings > API` for your project.
![Supabase settings](/img/guides/integrations/auth0/r1GAfLo.png)
Restart your Next.js development server to read in the new values from `.env.local`.
```bash
npm run dev
```
## Step 6: Install Auth0 Next.js library
Install the `@auth0/nextjs-auth0` library.
```bash
npm i @auth0/nextjs-auth0
```
Create a new file `pages/api/auth/[...auth0].js` and add:
```jsx
// 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:
```jsx
// pages/_app.js
import React from 'react'
import { UserProvider } from '@auth0/nextjs-auth0'
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.
```jsx
// 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](/img/guides/integrations/auth0/xLRL7S7.png)
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](/img/guides/integrations/auth0/YdBKRy6.png)
## 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](https://auth0.com/docs/configure/applications/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.
```bash
npm i jsonwebtoken
```
Update `pages/api/auth/[...auth0].js` with the following:
```jsx
// 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_SIGNING_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:
```jsx
// utils/supabase.js
import { createClient } from '@supabase/supabase-js'
const getSupabase = (access_token) => {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
)
if (access_token) {
supabase.auth.session = () => ({
access_token,
})
}
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!
```jsx
// 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.
```jsx
// 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](/img/guides/integrations/auth0/XgEMwnN.png)
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`.
```sql
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](https://www.youtube.com/watch?v=MJZCCpCYEqk).
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](/img/guides/integrations/auth0/M7XyhHe.png)
From the modal, select `Create a policy from scratch` and add the following.
![Policy settings for SELECT](/img/guides/integrations/auth0/wuWz3am.png)
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](https://www.youtube.com/watch?v=Ow_Uzedfohk).
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](/img/guides/integrations/auth0/dLOvhdq.png)
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](/img/guides/integrations/auth0/GdXS013.png)
Copy their `user_id`.
![User ID in Auth0 dashboard](/img/guides/integrations/auth0/tbvd0Uj.png)
Update each row in Supabase.
![User ID set to Auth0 user](/img/guides/integrations/auth0/tPu4Tt8.png)
Now when we refresh our application, we should finally see our list of `todos`!
> Note: Check out [the repo](https://github.com/dijonmusters/supabase-auth0-example/blob/main/pages/index.js) for an example of writing new `todos` to Supabase.
## Resources
- [Auth0](https://auth0.com/) official website.
- [Auth0 blog](https://auth0.com/blog/).
- [Using Next.js and Auth0 with Supabase article](https://auth0.com/blog/using-nextjs-and-auth0-with-supabase/).
- [Auth0 community](https://community.auth0.com/).
- [Auth0 documentation](https://auth0.com/docs/).