はじめに
Next.js(App Router)でSupabaseの認証を使う方法を画像付きで丁寧にまとめる😊
初めて触るときにつまづいたポイントも解説する✨
対象読者
- App Router × Supabase の組み合わせで使いたい!
- 素早く認証機能を作りたい!
この記事で解説すること
- 環境構築
- Next.js(App Router)のプロジェクトを新規作成する。
- そこにSupabaseを導入する。
- App RouterでSupabaseの認証を使う方法
- 基本的なサインアップ、ログインのやり方を解説する。
- 完成イメージ
「メールアドレス」と「パスワード」でサインアップ、ログインする最小限のページ。
つまづいたポイント
Supabaseを使うためのパッケージが2種類あり使い分けが分からなかった💦
結論
✅古いパッケージから新しいパッケージに移行している途中らしいので、今回は新しいパッケージを使う!
- 古いパッケージ「auth-helpers」
このパッケージは使わない!(検索するとこの情報が多くヒットする)
- 新しいパッケージ「@supabase/ssr」
このパッケージを使う!(新しい方に移行中らしいのでこっちを使う)
環境構築
使用する技術
事前にインストールしておくもの
- Node.js
- npm
今からインストールするもの
- Next.js
- Supabase
Next.jsのプロジェクトを新規作成
supabase-sample-app
という名前のプロジェクトを作成する。
npx create-next-app supabase-sample-app --ts --no-tailwind --eslint --app --src-dir --import-alias '@/*'
作成するとこんな感じになる。
プロジェクトに移動
supabase-sample-app
に移動する。
cd supabase-sample-app
Supabaseのパッケージをインストール
- JavaScript上でSupabaseを使うためのパッケージをインストールする。
npm install @supabase/supabase-js
- Next.jsでSupabaseを使うためのヘルパーをインストールする。
npm install @supabase/ssr
- CLIのパッケージもインストールする。
npm install supabase --save-dev
※Supabaseのローカル開発環境を作るとき、テーブルの型定義ファイルを生成するときなどに必要。
Supabaseのプロジェクトを作成
環境変数を設定
「Home」を開いて、少し下にスクロールすると「URL」と「API Key」がある。
ルートディレクトリに「.env.local」という名前の空ファイルを作成する。
そこに「URL」と「API Key」を記載する。
NEXT_PUBLIC_SUPABASE_URL=先ほど確認したURL
NEXT_PUBLIC_SUPABASE_ANON_KEY=先ほど確認したanon Key
こんな感じ✨
プログラム作成
基本的には公式ドキュメントのとおりでOK!
ここでは画像と補足付きで分かりやすく解説する。
①Supabaseクライアントを準備
Supabaseクライアントとは
プログラム上でSupabaseを扱うには「Supabaseクライアント」と呼ばれる変数が必要。
【手順1】Supabaseクライアントを生成する関数を作成
✅Supabaseクライアントを生成する汎用的な関数を作っておく。
src/app/utils/supabase/server.ts(公式ドキュメントのコピペでOK)
※utils、supabaseフォルダは新規作成。
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";
export const createClient = () => {
const cookieStore = cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value;
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options });
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: "", ...options });
} catch (error) {
// The `delete` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
},
);
};
②認証で必要なミドルウェアを作成
✅公式ドキュメントに沿ってミドルウェアを作っていく。
ミドルウェアの必要性
- サーバーコンポーネント
Cookie(認証トークン) を書き込むことができない💦
- ミドルウェア
Cookie(認証トークン) を書き込むことができる✨
【手順1】認証トークンをリフレッシュする関数を作成
✅ミドルウェアで使うリフレッシュ関数を準備する。
src/utils/supabase/middleware.ts(公式ドキュメントのコピペでOK)
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
// 期限切れの認証トークンをリフレッシュ
export async function updateSession(request: NextRequest) {
// 初期のレスポンスを設定
let response = NextResponse.next({
request: {
headers: request.headers,
},
})
// Supabaseのサーバークライアントを作成
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
// クッキーを取得する関数
get(name: string) {
return request.cookies.get(name)?.value
},
// クッキーを設定する関数
set(name: string, value: string, options: CookieOptions) {
// リフレッシュした認証トークンをサーバーコンポーネントに渡す
request.cookies.set({
name,
value,
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
// リフレッシュした認証トークンをブラウザに渡す
response.cookies.set({
name,
value,
...options,
})
},
// クッキーを削除する関数
remove(name: string, options: CookieOptions) {
// リフレッシュした認証トークンをサーバーコンポーネントに渡す
request.cookies.set({
name,
value: '',
...options,
})
response = NextResponse.next({
request: {
headers: request.headers,
},
})
// リフレッシュした認証トークンをブラウザに渡す
response.cookies.set({
name,
value: '',
...options,
})
},
},
}
)
// 現在のユーザーを取得(認証トークンをリフレッシュ)
await supabase.auth.getUser()
// 更新されたレスポンスを返す
return response
}
【手順2】ミドルウェアを作成
先ほど作った関数を呼び出す。
src/middleware.ts(公式ドキュメントのコピペでOK)
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware(request: NextRequest) {
// 期限切れの認証トークンをリフレッシュ
return await updateSession(request)
}
export const config = {
matcher: [
/*
* 以下3つのパスを除くすべてのリクエストでミドルウェアを適用する。
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* 適宜変更してもOK
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
③ログイン、サインアップ
✅公式ドキュメントに沿って最小限のログイン、サインアップを作っていく。
【手順1】ページ作成
✅シンプルなフォームを作るだけ。
src/app/login/page.tsx(公式ドキュメントのコピペでOK)
※loginフォルダは新規作成。
import { login, signup } from './actions' // まだ作っていない。後で作る。
export default function LoginPage() {
return (
<form>
<label htmlFor="email">Email:</label>
<input id="email" name="email" type="email" required />
<label htmlFor="password">Password:</label>
<input id="password" name="password" type="password" required />
{/* ✅Server Actionsでログイン、サインアップ */}
<button formAction={login}>Log in</button>
<button formAction={signup}>Sign up</button>
</form>
)
}
【解説】Server Actionsでログイン、サインアップ
{/* ✅Server Actionsでログイン、サインアップ */}
<button formAction={login}>Log in</button>
<button formAction={signup}>Sign up</button>
- 「Log in」ボタンを押すとlogin関数を実行する。
- 「Sign up」ボタンを押すとsignup関数を実行する。
【手順2】ログイン、サインアップの処理(Server Actions)
✅ボタンを押したときの処理(ログイン、サインアップ)を作る。
src/app/login/actions.ts(公式ドキュメントのコピペでOK)
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
/**
* ログイン
*
* ログインが成功した場合はトップページへリダイレクトする。
* ログインに失敗した場合はエラーページへリダイレクトする。
*
* @param formData - フォームから受け取ったデータ
* @returns void
*/
export async function login(formData: FormData) {
// ✅Supabaseクライアント
const supabase = createClient()
// フォームからデータ取得
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
// ✅ログイン
const { error } = await supabase.auth.signInWithPassword(data)
// ログインエラーの場合
if (error) {
redirect('/error') // 「/error」はまだ作っていない。後で作る。
}
// トップページのlayoutを再検証
revalidatePath('/', 'layout')
// トップページへリダイレクト
redirect('/')
}
/**
* サインアップ
*
* サインアップが成功した場合はトップページへリダイレクトする。
* サインアップに失敗した場合はエラーページへリダイレクトする。
*
* @param formData - フォームから受け取ったデータ
* @returns void
*/
export async function signup(formData: FormData) {
// ✅Supabaseクライアント
const supabase = createClient()
// フォームからデータ取得
const data = {
email: formData.get('email') as string,
password: formData.get('password') as string,
}
// ✅サインアップ
const { error } = await supabase.auth.signUp(data)
// サインアップエラーの場合
if (error) {
redirect('/error') // 「/error」はまだ作っていない。後で作る。
}
// トップページのlayoutを再検証
revalidatePath('/', 'layout')
// トップページへリダイレクト
redirect('/')
}
【解説】Supabaseクライアント
// ✅Supabaseクライアント
const supabase = createClient()
- ログインやサインアップをするには「Supabaseクライアント」が必要。
【解説】ログイン
// ✅ログイン
const { error } = await supabase.auth.signInWithPassword(data)
signInWithPassword(…)
を使うだけでログインできる!- 引数にはログインに使う
email
とpassword
を指定する。
- 引数にはログインに使う
【解説】サインアップ
// ✅サインアップ
const { error } = await supabase.auth.signUp(data)
signUp(…)
を使うだけでサインアップできる!- 引数には今後ログインするときに使う
email
とpassword
を指定する。
- 引数には今後ログインするときに使う
【手順3】エラー画面を作成
✅ログインや、サインアップでエラーが発生したとき用のページを作る。
src/app/error/page.tsx(公式ドキュメントのコピペでOK)
※errorフォルダは新規作成。
export default function ErrorPage() {
return <p>Sorry, something went wrong</p>
}
【手順4】確認メールの内容を編集
デフォルトではサインアップするとき、入力したemail宛に確認メールが届く。
✅サーバー側で認証をするにはメールの内容を少し変更しないといけない。
- 管理画面のメール編集ページへアクセスする。
- プロジェクトを選択する。
- メールの内容を書き換える。
変更前
<h2>Confirm your signup</h2> <p>Follow this link to confirm your user:</p> <p><a href="{{ .ConfirmationURL }}">Confirm your mail</a></p>
変更後(公式ドキュメントのコピペでOK)
<h2>Confirm your signup</h2> <p>Follow this link to confirm your user:</p> <p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup">Confirm your mail</a></p>
変更点
- aタグのhrefを
"{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup"
に変更した。{{ .SiteURL }}
は、サイトのURL。{{ .TokenHash }}
は、確認に使うハッシュ化されたトークン。
【手順5】確認用の処理を作成
✅確認メールのリンク先の処理を実装する。
src/app/auth/confirm/route.ts(公式ドキュメントのコピペでOK)
※auth、confirmフォルダは新規作成。
import { type EmailOtpType } from '@supabase/supabase-js'
import { type NextRequest, NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'
// 確認処理
export async function GET(request: NextRequest) {
// URLからパラメータを取得
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type') as EmailOtpType | null
const next = searchParams.get('next') ?? '/'
// リダイレクト先のURLを設定
const redirectTo = request.nextUrl.clone()
redirectTo.pathname = next
redirectTo.searchParams.delete('token_hash')
redirectTo.searchParams.delete('type')
// token_hashとtypeが存在する場合
if (token_hash && type) {
// Supabaseクライアントを作成
const supabase = createClient()
// ✅OTP(ワンタイムパスワード)を検証
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
// エラーがない場合、リダイレクト先へ移動
if (!error) {
redirectTo.searchParams.delete('next')
return NextResponse.redirect(redirectTo)
}
}
// エラーページへリダイレクト
redirectTo.pathname = '/error'
return NextResponse.redirect(redirectTo)
}
【解説】OTP(ワンタイムパスワード)を検証
// ✅OTP(ワンタイムパスワード)を検証
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
verifyOtp(…)
で正規のサインアップか判定する。- typeが”signup”になっているか?
- token_hashがメールに記載したハッシュ化したトークンになっているか?
動作確認
これでログイン、サインアップが使えるようになっているので実際に動かしてみる。
- アプリを起動
npm run dev
- ページを開く
- emailとpasswordを入力してサインアップ
- するとトップページにリダイレクトされる
- このとき確認メールが届くのでリンクをクリックする
- するとトップページが開く
(内部的にはサインアップ → トップページへリダイレクト が発生)
- Supabaseの管理画面を見てみるとユーザーが登録されていることが分かる!
その他の処理(ログアウトなど)
✅同じく「Supabaseクライアント」を使えば簡単に実装できる。
注意点
確認メールは1時間あたり3通しか送れない
テスト用のプログラムなら1時間あたり3通でも問題ないが、本番運用する場合は3通だと足りない💦
この上限は自分のSMTPサーバーを設定することで回避できる✨
(別途SMTPサーバーの契約が必要)
まとめ
今回は「メールアドレス」と「パスワード」でのログイン、サインアップを解説した。
同じような要領でGoogleなどでのソーシャルログインも簡単に作ることもできる✨
ポイント
- パッケージは「auth-helpers」ではなく「@supabase/ssr」を使う!
- 「Supabaseクライアント」と呼ばれる変数を使ってログイン、サインアップをする。
ログイン、サインアップに必要なこと
- 認証を使うためのミドルウェアを作る。
- emailとpasswordのフォームを作る。
- Server Actionsにログイン、サインアップ処理を作る。
(Server Actionsの代わりにルートハンドラなどに書いてもOK)
- サインアップするときに送られる確認メールの内容を変更する。
- 確認メールで使う確認処理を作る。
参考サイト
公式ドキュメント(認証)
公式ドキュメント(認証のチュートリアル)
公式ドキュメント(認証 ― Next.jsに導入する方法)
公式ドキュメント(認証で使える関数 ― JavaScript)
実践例
セキュリティ上の注意