レビュー済み·難易度: 中級·更新: 2026-04-16

Supabase でメール・パスワード認証を実装する

Next.js App Router + Supabase Auth を使ったメール/パスワード認証の実装例。サインアップ・ログイン・ログアウト・セッション管理(Server Components からの参照)を扱う。

nextjsauthenticationapisupabasezod

対応バージョン

nextjs 15typescript 5@supabase/supabase-js 2@supabase/ssr 0.5

前提環境

Next.js App Router の基本と Server Actions の使い方を理解していること。Supabase プロジェクトが作成済みであること。

概要

Next.js App Router で Supabase Auth を使いメール/パスワード認証を実装する。 @supabase/ssr パッケージを使って Server Components・Server Actions・Middleware でセッションを共有する構成。

インストール

npm install @supabase/supabase-js @supabase/ssr

環境変数の設定

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Supabase クライアントの作成

App Router では「サーバー用」と「ブラウザ用」を使い分ける。

// src/lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => {
            cookieStore.set(name, value, options);
          });
        },
      },
    }
  );
}
// src/lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
}

Middleware でセッションを更新する

// middleware.ts
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            request.cookies.set(name, value)
          );
          supabaseResponse = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          );
        },
      },
    }
  );

  // セッションを更新してトークンの有効期限切れを防ぐ
  await supabase.auth.getUser();

  return supabaseResponse;
}

export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};

サインアップ・ログイン(Server Actions)

// src/app/auth/actions.ts
"use server";

import { redirect } from "next/navigation";
import { z } from "zod";
import { createClient } from "@/lib/supabase/server";

const authSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export async function signUp(formData: FormData) {
  const parsed = authSchema.safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
  });
  if (!parsed.success) return { error: "入力内容を確認してください" };

  const supabase = await createClient();
  const { error } = await supabase.auth.signUp(parsed.data);

  if (error) return { error: error.message };
  redirect("/dashboard");
}

export async function signIn(formData: FormData) {
  const parsed = authSchema.safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
  });
  if (!parsed.success) return { error: "入力内容を確認してください" };

  const supabase = await createClient();
  const { error } = await supabase.auth.signInWithPassword(parsed.data);

  if (error) return { error: "メールアドレスまたはパスワードが正しくありません" };
  redirect("/dashboard");
}

export async function signOut() {
  const supabase = await createClient();
  await supabase.auth.signOut();
  redirect("/login");
}

ログインフォーム

// src/app/login/page.tsx
import { signIn } from "@/app/auth/actions";

export default function LoginPage() {
  return (
    <form action={signIn} className="space-y-4 max-w-sm mx-auto mt-16">
      <h1 className="text-xl font-bold">ログイン</h1>

      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          メールアドレス
        </label>
        <input
          id="email"
          name="email"
          type="email"
          required
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
      </div>

      <div>
        <label htmlFor="password" className="block text-sm font-medium">
          パスワード
        </label>
        <input
          id="password"
          name="password"
          type="password"
          required
          className="mt-1 block w-full rounded border px-3 py-2 text-sm"
        />
      </div>

      <button
        type="submit"
        className="w-full rounded bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700"
      >
        ログイン
      </button>
    </form>
  );
}

Server Component でセッションを参照する

// src/app/dashboard/page.tsx
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import { signOut } from "@/app/auth/actions";

export default async function DashboardPage() {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  // 未ログインはログインページへ
  if (!user) redirect("/login");

  return (
    <div className="p-8">
      <p className="text-sm text-gray-600">ログイン中: {user.email}</p>
      <form action={signOut} className="mt-4">
        <button
          type="submit"
          className="rounded border px-4 py-2 text-sm hover:bg-gray-50"
        >
          ログアウト
        </button>
      </form>
    </div>
  );
}

ポイント

  • @supabase/ssrcreateServerClient / createBrowserClient を用途ごとに使い分ける
  • Middleware でセッション更新を行うことでトークンの自動リフレッシュが機能する
  • Server Actions から redirect() を使うことで認証後のリダイレクトをサーバー側で完結できる
  • Server Components では getUser() でセッションを確認し、未認証なら即 redirect()
  • エラーメッセージは内部エラーをそのまま返さず、ユーザー向けに丸める

注意点

Supabase の URL / anon key は .env.local に設定すること。本番環境では NEXT_PUBLIC_ 変数の扱いに注意する。

関連サンプル