メインコンテンツまでスキップ

Auth0

このガイドでは、Auth0とSupabaseを使ってNext.jsのアプリケーションを構築する手順を説明します。Auth0でユーザーの認証とトークンの管理をし、Supabaseで行単位セキュリティーポリシーを使った認可ロジックを記述します。

メモ:このガイドは、Auth0のブログに掲載されている「Using Next.js and Auth0 with Supabase」(Supabaseと一緒にNext.jsとAuth0を利用)の記事を参考にしています。Auth0とSupabaseの統合に関する実践的なステップバイステップのガイドをやってみましょう。

このガイドの完全なコード例はこちらにあります。

Auth0は、認証と認可のプラットフォームであり、ユーザーを認証、管理するための様々な戦略を提供します。ユーザーがアプリケーションにサインインする方法、生成されるトークン、ユーザーに関するデータの保存などを細かく制御できます。

Next.jsは、React上に構築されたWebアプリケーションフレームワークです。この例では、アプリケーション内にサーバーサイドのロジックを書くのに使用します。Auth0は、Next.js専用に非常によく統合された認証ライブラリーを実装しています。

メモ:Next.jsのAPIルート(サーバーレス・ファンクション)は、Express、Koa、FastifyなどのNodeサーバーのフレームワーク構造によく似ています。このガイドで紹介するサーバーサイドのロジックは、これらのフレームワークでも簡単に動作するようにでき、フロントエンドとは別のアプリケーションとして管理できます。

Auth0のアカウントをお持ちでない方は、こちらでアカウントを作成してください。

また、Supabaseのアカウントも必要なので、こちらからサインインして作成します。

ステップ1:Auth0のテナントを作成

Auth0のダッシュボードから、Auth0のロゴの右にあるメニューをクリックして、Create tenant(テナントを作成)を選択します。

Create tenant from Auth0 dashboard

テナントのDomain(ドメイン)を入力してください - これは一意になる必要があります。

Regionを選択してください - 想定されるユーザーの地理的に近い地域を選択してください。

Environment TagDevelopmentを選択します。本番稼動の準備ができた時点でproductionにします。

Auth0 tenant settings

ステップ2:Auth0アプリケーションを設定

サイドバーメニューからApplications > Applicationsを選択し、Create Applicationをクリックします。

アプリケーションに名前を付け、Regular Web Applicationsを選択し、Createをクリックします。

Auth0 application settings

Settingsを選択しApplication URIsセクションに移動して、以下を更新します。

Allowed Callback URLS(許可するコールバックURL):http://localhost:3000/api/auth/callback

Allowed Logout URLs(許可するログアウトURL):http://localhost:3000

Settingsセクションの一番下までスクロールし、Advanced Settingsを表示します。

OAuthを選択し、JSON Web Token SignatureRS256に設定します。

OIDC ConformantEnabledになっていることを確認します。

Saveをクリックして設定を更新します。

ステップ3:Supabaseプロジェクトの作成

Supabaseダッシュボードから、New projectをクリックします。

NameにSupabaseプロジェクトの名前を入力します。

Database Passwordにセキュアなパスワードを入力します。

Auth0テナントで選択したのと同じRegionを選択します。

Create new projectをクリックします。

New Supabase project settings

ステップ4:Supabaseでデータを作成

Supabase ダッシュボードのサイドバーメニューから、Table editorから、New tableの順にクリックします。

Name欄にtodoと入力します。

Enable Row Level Security (RLS)を選択します。

2つの新しい列を作成します。

  • titletextとして
  • user_idtextとして
  • is_completeboolとしてデフォルト値にfalse

Savaをクリックして、新しいテーブルを作成します。

Todo table

Table editortodoテーブルを選択し、Insert rowをクリックします。

titleフィールドを入力し、Saveをクリックします。

New row settings

Insert rowをクリックして、いくつかのTodoを追加します。

List of todos

ステップ5:Next.jsアプリを構築

新しいNext.jsプロジェクトを作成します。

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

.env.localファイルを作成し、以下の値を入力します。

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

メモ:Auth0の値は、Settings > Basic Infomationで確認できます。

Auth0 settings

メモ:Supabaseの値は、プロジェクトのSettings > APIで確認できます。

Supabase settings

Next.jsの開発サーバーを再起動すると、.env.localから新しい値が読み込まれます。

npm run dev

ステップ6:Auth0 Next.jsライブラリをインストール

@auth0/nextjs-auth0ライブラリをインストールします。

npm i @auth0/nextjs-auth0

新しいファイルpages/api/auth/[...auth0].jsを作成し、次のコードを追加します。

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

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

export default handleAuth()

メモ:これにより、いくつかのAPIルートが作成されます。主に使用するのは、/api/auth/login/api/auth/logoutで、ユーザーのログインとログアウトを処理します。

pages/_app.jsを開き、Auth0のUserProviderComponentをラップします。

// 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

pages/index.jsを更新して、ユーザーがランディングページを見るためにログインしていることを確認します。

// 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

メモ:withPageAuthRequiredは、ユーザーが現在ログインしていない場合、自動的に/api/auth/loginにリダイレクトします。

これが機能しているかどうかは、http://localhost:3000に移動して、Auth0のサインイン画面にリダイレクトされることを確認します。

Auth0 sign in screen

新しいアカウントでSign upか、Continue with Googleをクリックしてサインインしてください。

これでランディングページを見ることができます。

Landing page

ステップ7:Supabase用のAuth0トークンを登録

現在のところ、SupabaseとAuth0のどちらも、JWTにカスタムの署名シークレットを設定できません。また、これらは異なる署名アルゴリズムを使用しています。

そのため、Auth0のJWTから必要なビットを抽出し、自分で署名してSupabaseに送信する必要があります。

これには、Auth0のafterCallback関数を使用します。

jsonwebtokenライブラリをインストールします。

npm i jsonwebtoken

pages/api/auth/[...auth0].jsを以下のように更新します。

// 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)
}
},
})

JWTのpayloadには、Auth0のユーザーの一意の識別子(session.user.sub)と、1時間の有効期限が含まれます。

Supabaseの署名シークレットを使ってこのJWTに署名します。

メモ:afterCallback関数を実行し、新しいトークンを作成するために、ユーザーを一旦サインアウトし、再度サインインする必要があります。

あとは、このトークンをSupabaseにリクエストと一緒に送信するだけです。

ステップ8:Supabaseにデータを要求

utils/supabase.jsという新しいファイルを作成し、以下の内容を追加します。

// 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 }

これは、Supabaseとやりとりするためのクライアントになります。これにaccess_tokenを渡せば、リクエストに付与されます。

Supabaseからtodosをランディングページに読み込んでみましょう。

// 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

あるいは、getServerSideProps関数を使ってサーバー上のtodosも取得できます。

// 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

どちらにしても、アプリケーションをリロードすると、todosの状態が空になっています。

Empty todo list

これは、デフォルトですべてのリクエストをブロックする行単位セキュリティーを有効にしたからです。ユーザーがtodosをselectできるようにするには、ポリシーを書く必要があります。

ステップ9:selectを許可するためのポリシーを書く

ポリシーには、現在ログインしているユーザーが誰であるかを知り、そのユーザーにアクセス権を与えるかどうかを決定する必要があります。新しいJWTから現在のユーザーを抽出するために、PostgreSQLの関数を作りましょう。

Supabaseダッシュボードに戻り、サイドバーメニューからSQLを選択し、New queryをクリックします。これにより、new sql snippetという新しいクエリが作成され、Postgresデータベースに対して任意のSQLを実行できるようになります。

以下のように記述し、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;

これにより、auth.user_id()という関数が作成され、JWTペイロードのuserIdフィールドを検査できます。

メモ:PostgreSQLの関数について詳しく知りたい方は、こちらのビデオをご覧ください。

このユーザーがTodoの所有者であるかどうかをチェックするポリシーを作成しましょう。

Supabaseのサイドバー メニューからAuthenticationを選択し、Policies をクリックして、todoテーブルのNew Policyをクリックします。

Create new policy

モーダルからCreate a policy from scratchを選択し、以下を追加します。

Policy settings for SELECT

このポリシーは、先ほど作成した関数を呼び出して、現在ログインしているユーザーのIDであるauth.user_id()を取得します。これが現在のtodouser_id列と一致するかどうかをチェックして、一致した場合はユーザーのselectを許可し、一致しない場合は拒否し続けます。

Reviewをクリックし、Save policyをクリックします。

メモ:RLSとポリシーの詳細については、認証詳細のビデオをご覧ください。

最後にやるべきことは、既存のtodosuser_id列を更新することです。

Supabaseのダッシュボードに戻って、サイドバーからTable editorを選択します。

User ID null in Supabase Table Editor

user_id列がすべてNULLになっています。

Auth0ユーザーのIDを取得するには、Auth0ダッシュボードに移動します。サイドバーからUser Managementを選択し、Usersをクリックして、テストユーザーを選択します。

List of users in Auth0 dashboard

そのユーザーのuser_idをコピーします。

User ID in Auth0 dashboard

Supabaseの各行を更新します。

User ID set to Auth0 user

これで、アプリケーションを更新すると、ようやくtododsのリストが表示されるはずです。

メモ:Supabaseに新しいtodosを書き込む例はこちらのリポジトリーをご覧ください。

リソース