🐙

Next.jsとSupabaseで簡単にユーザー認証を実装する!

に公開

前提条件

  • Next.jsのバージョン15.3.4
  • Supabaseでユーザー登録をしておく

Supabaseの初期設定を行う

事前準備

https://supabase.com
New Organizationから新しく組織を作成
New projectから新しくプロジェクトを作成

手順

Authenticationを選択

Authenticaiton内のSign in /Providersを選択

Auth ProvidersEmailのEnabledを選択

有効にしたいルールにチェックをつける

デフォルトでは以下の3つが有効になっている

  • Enable Email Provider
    • メール認証を有効にする
  • Confirm email
    • 有効にすればサインインの際にユーザーのメールアドレスで認証する
  • Secure email change
    • メール変更時の設定

Next.jsのコード

まずはsupabaseに必要なパッケージをインストール

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

ファイル構成は以下の通り

src/
├── middleware.ts                 # Next.js ミドルウェア
├── lib/
│   ├── supabaseMiddleware.ts     # Supabase ミドルウェア関数
│   ├── supabase-server.ts        # サーバーサイド Supabase クライアント
│   ├── supabase.ts               # クライアントサイド Supabase クライアント
│   └── auth.ts                   # 認証ヘルパー関数
├── app/
│   ├── login/
│   │   └── page.tsx              # ログインページ
│   └── page.tsx                  # ホームページ
└── components/
    └── ...                       # 各種コンポーネント

まず、supabase/ssrではサーバーコンポーネントとクライアントコンポーネントで認証に必要なコードが異なる。

サーバーコンポーネント向けの認証

  1. ミドルウェアの実装
middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from './lib/supabaseMiddleware'

export async function middleware(request: NextRequest) {
    return await updateSession(request)
}

export const config = {
    matcher: [
        '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
    ],
}
  1. サーバーサイド Supabase クライアントの実装
src/lib/supabase-server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createServerSupabaseClient() {
    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) {
                    try {
                        cookiesToSet.forEach(({ name, value, options }) =>
                            cookieStore.set(name, value, options)
                        )
                    } catch {
                        // The `setAll` method was called from a Server Component.
                        // This can be ignored if you have middleware refreshing
                        // user sessions.
                    }
                },
            },
        }
    )
}
  1. Supabase ミドルウェア関数を実装
src/lib/supabaseMiddleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(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, }) => request.cookies.set(name, value))
                    supabaseResponse = NextResponse.next({
                        request,
                    })
                    cookiesToSet.forEach(({ name, value, options }) =>
                        supabaseResponse.cookies.set(name, value, options)
                    )
                },
            },
        }
    )
    const {
        data: { user },
    } = await supabase.auth.getUser()

    if (
        !user &&
        !request.nextUrl.pathname.startsWith('/login') &&
        !request.nextUrl.pathname.startsWith('/auth')
    ) {
        const url = request.nextUrl.clone()
        url.pathname = '/login'
        return NextResponse.redirect(url)
    }
    return supabaseResponse
}
  1. 認証ヘルパー関数の実装
src/lib/auth.ts
import { createServerSupabaseClient } from "@/lib/supabase-server";
import { redirect } from "next/navigation";

export async function requireAuth() {
    const supabase = await createServerSupabaseClient();
    const { data: { user }, error } = await supabase.auth.getUser();

    if (error || !user) {
        console.log('認証失敗、ログインページにリダイレクト');
        redirect('/login');
    }

    return user;
}
  1. サーバーコンポーネントで使う
src/app/page.tsx
import { requireAuth } from "@/lib/auth";
import Header from "@/components/Header";

export default async function HomePage() {
  // 認証チェック
  const user = await requireAuth();
  console.log('ユーザー情報:', { user: user?.email });

  return (
    <>
      <Header />
      <main>
        <h1>ようこそ、{user.email}さん</h1>
        {/* ページコンテンツ */}
      </main>
    </>
  );
}

クライアントコンポーネント向けの認証

  1. クライアントサイド Supabase クライアントの実装
src/lib/supabase.ts
import { createBrowserClient } from '@supabase/ssr'

// クライアントサイド用
export const createClient = () => {
    return createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    )
}
  1. クライアントコンポーネントでの使用例
src/app/login/page.tsx
"use client";
import { useState } from "react";
import { createClient } from "@/lib/supabase";
import { useRouter } from "next/navigation";

export default function LoginPage() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");
    const [message, setMessage] = useState("");
    const [isLoading, setIsLoading] = useState(false);
    const router = useRouter();

    const handleLogin = async () => {
        setIsLoading(true);
        const supabase = createClient();

        const { error } = await supabase.auth.signInWithPassword({
            email,
            password
        });

        if (error) {
            setMessage(`ログインに失敗しました: ${error.message}`);
        } else {
            setMessage("ログインしました");
            router.push("/");
        }
        setIsLoading(false);
    };

    return (
        <div className="p-4">
            <h2>ログイン</h2>
            <input
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="メールアドレス"
                className="border p-2 w-full mb-2"
            />
            <input
                type="password"
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                placeholder="パスワード"
                className="border p-2 w-full mb-2"
            />
            <button
                onClick={handleLogin}
                disabled={isLoading}
                className="bg-blue-600 text-white px-4 py-2 rounded w-full"
            >
                {isLoading ? "処理中..." : "ログイン"}
            </button>
            {message && <p className="mt-2">{message}</p>}
        </div>
    );
}

Supabaseの認証(サーバーvsクライアント)比較

サーバーサイド認証 クライアントサイド認証
実行環境 サーバー上 ブラウザ
コンポーネントの種類 サーバーコンポーネント クライアントコンポーネント
セッション管理 読み取りのみ可能 読み取り、書き込み可能
データアクセス 直接アクセス可能 API経由でアクセス可能

サーバーサイド認証フロー

  1. ユーザーがページにアクセス
  2. ミドルウェアがリクエストを処理し、認証状態をチェック
  3. 未認証の場合はログインページにリダイレクト
  4. 認証済みの場合はサーバーコンポーネントが実行され、ユーザーデータを取得
  5. レンダリングされたHTMLがクライアントに送信される

クライアントサイド認証フロー

  1. ユーザーがページにアクセス
  2. 初期HTMLがロードされる
  3. Javascriptが実行され、クライアントコンポーネントがマウントされる
  4. クライアントコンポーネントがSupabaseクライアントを初期化
  5. 認証状態をチェックし、UIを更新
  6. ーザーがログインボタンをクリックすると、認証リクエストが送信される
  7. 認証成功後、UIが更新される

Supabase 認証の使い分け: 簡単な考え方

基本原則は以下の通り

  1. 読み取りはサーバーサイド
  • データの取得や表示はサーバーコンポーネントで行う
    • 例: ユーザープロフィールの表示、投稿一覧の取得
  1. 書き込みはクライアントサイド
  • ユーザーの操作による変更はクライアントコンポーネントで行う
    • 例: ログイン/ログアウト、フォーム送信、いいね機能
  1. 保護はミドルウェア
  • ページの保護や認証チェックはミドルウェアで行う
    • 例: 未ログインユーザーのリダイレクト

実際の例

やりたいこと どちらを使うか 理由
ユーザーがログインしているか確認 サーバーサイド 初期表示に必要な情報だから
ログインボタンを実装 クライアントサイド ユーザーの操作に応じて処理するから
プロフィールデータを表示 サーバーサイド 読み取り操作だから
プロフィール更新フォーム クライアントサイド フォーム送信という書き込み操作だから
認証が必要なページを保護 ミドルウェア ページアクセス前に確認する必要があるから

Discussion