Supabase Authを使ったNext.jsアプリの認証と認可(1/2)
前回、Next.jsでSupabaseを使って認証し、データを更新するまで手順をまとめた記事を作成しました。
前の記事では実装の手順だけを示しただけでしたので、今回はマジック・リンクによる認証を実装しながら、処理内容をもう少し詳しくみていきます。
認証と認可
Supabaseでは、Postgresを使って、Authの仕組みを提供しています。
Authの仕組みとして、大きく2つの要素があります。
- 認証:ユーザーを入れていいのか。入れた場合、そのユーザーは何者なのか。
- 認可:入ったユーザーは、何をできるのか。どのデータにアクセスや操作できるか。
最初は認証に関して、それから認可について、詳しく順にみていきます。
Supabaseプロジェクトを作成
まず、新しくSupabaseでプロジェクトを作成します。Supabaseのサインアップがまだの場合は、こちらより登録してください。
- Organization: プロジェクトが所属する組織を選択します。
- Name: プロジェクトの名前を入力します。
- Database Password: データベースに使用するパスワードを入力します。
- Region: 想定されるアプリを使用するユーザーにネットワーク的に近い地域を選択します。日本の場合は、「Northeast Asia (Tokyo)」(東アジア - 東京)を選択します。
- Pricing Plan: 1組織あたり2つまでは無料で作成できます。枠が残っている場合は、「Free tier」(無料枠)を選択できます。
「Create new project」ボタンをクリックして、新しくプロジェクトを作成します。
作成できるまでしばらく待ちます。
プロジェクトが作成できたら、後ほどアプリで必要になるキーとURLを保持します。「Settings」の「API」から次の箇所より取得できます。
- 「Project API keys」欄の「anon」キー
- 「Config」欄の「URL」
Next.jsアプリをセットアップ
Next.jsとSupbaseクライアントの初期設定を済ませたリポジトリーを用意したので、そちらをベースに手順を解説します。Tailwindcssの初期設定も含んでいるので、ボタンの体裁などのスタイルはそちらを利用します。
degit経由で取得して、必要なパッケージをインストールします。
npx degit hirotaka/examples/supabase-nextjs-database#atlanta-0.0.1 supabase-nextjs-database
cd supabase-nextjs-database
npm install
環境変数をセットするために.env.local
ファイルを作成します。
cp .env .env.local
先ほどSupbaseのダッシュボードで控えた、anonキーをNEXT_PUBLIC_SUPABAE_ANON_KEY
に、URLをNEXT_PUBLIC_SUPABASE_URL
にセットします。
次のような形式の記述になります。
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co
開発サーバーを起動します。
npm run dev
ブラウザでローカルのアドレスに接続します。
「Hello, world!」というメッセージが表示されます。
マジック・リンクで認証
それでは、準備ができたので、マジック・リンクによる認証を実装していきます。
コンテキストを作成
ユーザーのログインやログアウト処理やユーザーに関するセッションなどの情報をまとめてコンテキストに切り出しておきます。
contexts/user.tsx
という名前でファイルを作成します。
import { createContext, useState, useContext } from 'react'
import { supabase } from '@/lib/supabase-client'
const Context = createContext()
const Provider = ({ children }) => {
const [user, setUser] = useState()
const [loading, setLoading] = useState(false)
const login = async (email) => {
console.log('ログイン!')
setUser({ email })
}
const logout = async () => {
console.log('ログアウト!')
setUser(null)
}
const exposed = {
login,
logout,
user,
setUser,
loading,
}
return <Context.Provider value={exposed}>{children}</Context.Provider>
}
export const useUser = () => useContext(Context)
export default Provider
いったん、ログインとログアウトには仮の処理をいれておきます。
アプリ内のどのコンポーネントからでも呼び出せるように、pages/_app.tsx
にコンテキストのプロバイダをセットします。
import type { AppProps } from 'next/app'
import '@/styles/globals.css'
import UserProvider from '@/contexts/user'
function MyApp({ Component, pageProps }: AppProps) {
return (
<UserProvider>
<Component {...pageProps} />
</UserProvider>
)
}
export default MyApp
ログイン・コンポーネントを作成
ログイン・フォームを表示するコンポーネントを下記のようにします。
import { useState } from 'react'
import { useUser } from '@/contexts/user'
export default function Auth() {
const { loading, login } = useUser()
const [email, setEmail] = useState('')
return (
<div className="max-w-md mx-auto">
<h1 className="text-2xl">ログイン</h1>
<p className="mt-4">
Eメールを入力してマジック・リンクでログインします。
</p>
<div className="mt-6">
<label className="block">Eメール</label>
<input
className="mt-2 w-full"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<button
className="primary mt-4"
onClick={() => login(email)}
disabled={loading}
>
{loading ? '読み込み中...' : 'マジックリンクを送信'}
</button>
</div>
)
}
ダッシュボード・コンポーネントを作成
ログインした後に、表示するダッシュボードのコンポーネントを次のようにします。
import { useUser } from '@/contexts/user'
export default function Dashboard() {
const { logout, user } = useUser()
return (
<div className="max-w-md mx-auto">
<h1 className="text-2xl">ダッシュボード</h1>
<div className="mt-6">
<p>Hello, {user.email}!</p>
</div>
<div className="mt-6">
<button className="secondary" onClick={() => logout()}>
ログアウト
</button>
</div>
</div>
)
}
トップ画面を作成
トップ画面をユーザーの状態でコンポーネントを切り替えるようにします。
import Auth from '@/components/auth'
import Dashboard from '@/components/dashboard'
import { useUser } from '@/contexts/user'
export default function Home() {
const { user } = useUser()
return (
<div className="max-w-7xl p-10 m-auto">
{!user ? <Auth /> : <Dashboard />}
</div>
)
}
これで、Eメールを入力し、「マジック・リンクを送信」ボタンをクリックすると、ダッシュボードへ遷移する様になります。
認証処理を追加
続いて、実際にSupabaseでログインとログアウトができるように処理を実装します。
ログインのメソッドは次のようになります。
const login = async (email) => {
try {
setLoading(true)
const { error } = await supabase.auth.signIn({ email })
if (error) throw error
alert('ログインのリンクをEメールで確認してください!')
} catch (error) {
alert(error.error_description || error.message)
} finally {
setLoading(false)
}
}
auth.singIn()
メソッドにemail
を渡して呼びだすことで、マジック・リンクで認証します。
これで、ログイン処理が実行されてメールが送信されます。
Eメールのリンクをクリックして、遷移してきた際にuseEffect
で認証状態の変化を検知して、認証済みのユーザーをセットします。
useEffect(() => {
setUser(supabase.auth.user())
supabase.auth.onAuthStateChange(() => {
setUser(supabase.auth.user())
})
}, [])
onAuthStateChange()
で認証情報の変更を検知して、ユーザーをセットします。ユーザーがセットされると、pages/index.tsx
でコンポーネントをダッシュボードに切り替えます。
ブラウザーの再読み込みした際、auth.onAuthStateChange()
メソッドの中が実行されません。そのため、メッソドの外にも認証情報を取得してsetUser()
することでログイン状態を保つことができます。
ログアウト処理は、auth.signOut()
メソッドを呼びだして、ユーザーをクリアします。
const logout = async () => {
await supabase.auth.signOut()
setUser(null)
}
最終的に、contexts/user.tsx
ファイルは次のようになります。
ここまでのコード全体はこちらで確認できます。
動作を確認
はじめて、入力するEメールの場合は、登録の確認メールが送信されます。
Eメールのリンクをクリックするとユーザーとしてログインします。
ログアウトをして、もう一度、マジック・リンクを送信すると、マジック・リンクのEメールが送信されることを確認できます。
また、Eメールのリンクをクリックすることでログインできます。
これらのEメール本文を変更したい場合は、「Authentication」の「Template」で変更できます。
ユーザーの認証データについて
Supbaseのダッシュボードで、「Authentication」の「Users」で新しくユーザーが作成されているのを確認できます。
こここでは、よく使用される参照されるデータだけ表示されています。内部的にはいくつかのテーブルが存在し、auth
スキーマ内に作成されます。
「Table editor」のスキーマ選択で「auth」を選択すると、テーブルの中身を参照できます。
これらのテーブルを直接操作することはあまりないですが、どのようなテーブルやカラムが在るかを確認できます。
おわりに
マジック・リンクによる認証を実装しながら、Supbase Authの認証部分を詳細にみてきました。
次回は、ログインしたユーザーがデータベースのデータを操作するための認可部分の処理を紹介します。
参考
Supbaseの知識を深めるために、ドキュメントの翻訳に取り組んでいます。
Supabase Authに関しては、こちらも参考にしくてださい。
Discussion