はじめに
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-appSupabaseのパッケージをインストール
- 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 - ページを開く
URL:http://localhost:3000/login
 - 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)
実践例
セキュリティ上の注意


























