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
(テナントを作成)を選択します。
テナントのDomain
(ドメイン)を入力してください - これは一意になる必要があります。
Region
を選択してください - 想定されるユーザーの地理的に近い地域を選択してください。
Environment Tag
にDevelopment
を選択します。本番稼動の準備ができた時点でproductionにします。
ステップ2:Auth0アプリケーションを設定
サイドバーメニューからApplications > Applications
を選択し、Create Application
をクリックします。
アプリケーションに名前を付け、Regular Web Applications
を選択し、Create
をクリックします。
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 Signature
をRS256
に設定します。
OIDC Conformant
がEnabled
になっていることを確認します。
Save
をクリックして設定を更新します。
ステップ3:Supabaseプロジェクトの作成
Supabaseダッシュボードから、New project
をクリックします。
Name
にSupabaseプロジェクトの名前を入力します。
Database Password
にセキュアなパスワードを入力します。
Auth0テナントで選択したのと同じRegion
を選択します。
Create new project
をクリックします。
ステップ4:Supabaseでデータを作成
Supabase ダッシュボードのサイドバーメニューから、Table editor
から、New table
の順にクリックします。
Name
欄にtodo
と入力します。
Enable Row Level Security (RLS)
を選択します。
2つの新しい列を作成します。
title
はtext
としてuser_id
はtext
としてis_complete
はbool
としてデフォルト値にfalse
Sava
をクリックして、新しいテーブルを作成します。
Table editor
でtodo
テーブルを選択し、Insert row
をクリックします。
title
フィールドを入力し、Save
をクリックします。
Insert row
をクリックして、いくつかのTodoを追加します。
ステップ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
で確認できます。
メモ:Supabaseの値は、プロジェクトの
Settings > API
で確認できます。
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のUserProvider
でComponent
をラップします。
// 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のサインイン画面にリダイレクトされることを確認します。
新しいアカウントでSign up
か、Continue with Google
をクリックしてサインインしてください。
これでランディングページを見ることができます。
ステップ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の状態が空になっています。
これは、デフォルトですべてのリクエストをブロックする行単位セキュリティーを有効にしたからです。ユーザーが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 a policy from scratch
を選択し、以下を追加します。
このポリシーは、先ほど作成した関数を呼び出して、現在ログインしているユーザーのIDであるauth.user_id()
を取得します。これが現在のtodo
のuser_id
列と一致するかどうかをチェックして、一致した場合はユーザーのselectを許可し、一致しない場合は拒否し続けます。
Review
をクリックし、Save policy
をクリックします。
メモ:RLSとポリシーの詳細については、認証詳細のビデオをご覧ください。
最後にやるべきことは、既存のtodos
のuser_id
列を更新することです。
Supabaseのダッシュボードに戻って、サイドバーからTable editor
を選択します。
user_id
列がすべてNULL
になっています。
Auth0ユーザーのIDを取得するには、Auth0ダッシュボードに移動します。サイドバーからUser Management
を選択し、Users
をクリックして、テストユーザーを選択します。
そのユーザーのuser_id
をコピーします。
Supabaseの各行を更新します。
これで、アプリケーションを更新すると、ようやくtodods
のリストが表示されるはずです。
メモ:Supabaseに新しい
todos
を書き込む例はこちらのリポジトリーをご覧ください。