Menu

Next.js Project Supabase Auth Integration Guide

公式のSupabase Next.js認証ガイドとNexty.devテンプレートの実際のコードに基づいて、このドキュメントではNext.jsプロジェクトでのSupabase Authのコード統合手順と使用方法を詳しく説明します。

詳細な統合手順

ステップ1:依存関係のインストール

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

パッケージの説明:

  • @supabase/supabase-js - Supabase JavaScriptクライアントライブラリ
  • @supabase/ssr - サーバーサイドレンダリングに最適化されたヘルパーライブラリ

ステップ2:環境変数の設定

.env.localファイルを作成し、必要な環境変数を設定します:

# Supabase基本設定
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key

環境変数の詳細な説明については、以下をご覧ください:Environment Variables

ステップ3:Supabaseクライアントの作成

3.1 ブラウザクライアント

lib/supabase/client.ts
import { Database } from '@/lib/supabase/types'
import { createBrowserClient } from '@supabase/ssr'
 
export const createClient = () => {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

これはブラウザ環境専用のクライアントで、以下の用途に使用されます:

  • ユーザーインタラクションの処理(ログイン、登録、データ更新)
  • リアルタイムサブスクリプションデータ変更の監視
  • 認証状態管理の提供

3.2 サーバーサイドクライアント

lib/supabase/server.ts
import { Database } from '@/lib/supabase/types'
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
 
export async function createClient() {
  const cookieStore = await cookies()
 
  return createServerClient<Database>(
    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 {
            // Server Componentsでのsetall呼び出しは無視される
          }
        },
      },
    }
  )
}

これはサーバーサイドコンポーネント専用のクライアントで、以下の用途に使用されます:

  • Server Actionsでのデータベース操作
  • Route Handlersでのapi処理
  • Cookiesを通じたユーザーセッション管理

ステップ4:ミドルウェア認証の設定

まず、Supabaseの機能に基づいてミドルウェアが必要とするアクセス制御ロジックを完成させる必要があります。

lib/supabase/middleware.ts
/*
* これは中核となるロジックであり、完全なファイルコードではありません
*/
 
// 管理者権限が必要なルート
const ROLE_PROTECTED_ROUTES: Record<string, string[]> = {
  'dashboard/users': ['admin'],
};
 
// ログインが必要なルート
const PROTECTED_ROUTES = ['dashboard'];
 
export async function updateSession(request: NextRequest) {
  // ユーザー情報を取得
  const { data: { user } } = await supabase.auth.getUser()
  
  // ログイン状態をチェック
  if (!user && isProtectedRoute) {
    return NextResponse.redirect(new URL("/login", request.url))
  }
  
  // ロール権限をチェック
  if (user && needsRoleCheck) {
    const { data } = await supabase.from("users").select("role").eq("id", user.id).single();
    if (!allowedRoles.includes(data?.role || "user")) {
      return NextResponse.redirect(new URL("/403", request.url))
    }
  }
}

このファイルは認証状態管理ロジックを提供します:

  • updateSession()を通じてユーザー認証状態をチェックして更新
  • トークンの更新、セッション検証などを処理

また、アクセス制御ロジックも提供します:

  • 管理者専用ページは他のユーザーには禁止
  • ログインユーザーページは未認証ユーザーには禁止
  • コールバックアドレス付きでログインページにリダイレクトし、ログイン成功後に直接目標ページに入れるようにする

lib/supabase/middleware.tsmiddleware.tsにインポートして、上記のロジックを有効にします。

middleware.ts
import { updateSession } from '@/lib/supabase/middleware';
import createIntlMiddleware from 'next-intl/middleware';
import { NextRequest, NextResponse } from 'next/server';
import { routing } from './i18n/routing';
 
const intlMiddleware = createIntlMiddleware(routing);
 
export async function middleware(request: NextRequest): Promise<NextResponse> {
  // 1. まずSupabase認証を処理
  const supabaseResponse = await updateSession(request);
 
  if (supabaseResponse.headers.get('location')) {
    return supabaseResponse;
  }
 
  // 2. 次に国際化ルーティングを処理
  const intlResponse = intlMiddleware(request);
 
  // 3. cookieをマージ
  supabaseResponse.cookies.getAll().forEach((cookie) => {
    const { name, value, ...options } = cookie;
    intlResponse.cookies.set(name, value, options);
  });
 
  return intlResponse;
}

これにより完全なミドルウェア要件が完成します:

  • 期限切れの認証トークンを自動更新
  • 権限チェック、ログインが必要なルートを保護
  • ロール検証、管理者権限をチェック
  • 国際化統合

ステップ5:フロントエンド認証状態管理

[locale]/layout.tsxファイルで、<AuthProvider>を導入してグローバルなフロントエンド認証状態管理を実装します。

5.1 AuthProviderの中核機能

AuthProvider.tsx
type AuthContextType = {
  user: ExtendedUser | null;           // ロール情報を含むユーザーオブジェクト
  loading: boolean;                    // ロード状態
  signInWithGoogle: () => Promise<{ error: AuthError | null }>;
  signInWithGithub: () => Promise<{ error: AuthError | null }>;
  signInWithEmail: (email: string, captchaToken?: string) => Promise<{ error: AuthError | null }>;
  signOut: () => Promise<void>;
  refreshUser: () => Promise<void>;    // ユーザー情報を手動更新
};

5.2 拡張ユーザー情報

テンプレートはSupabaseのユーザータイプを拡張し、ロール情報を追加しています:

type ExtendedUser = User & {
  role: "admin" | "user";
};
 
const fetchUserRole = async (userId: string) => {
  const { data, error } = await supabase
    .from("users")
    .select("role")
    .eq("id", userId)
    .limit(1)
    .maybeSingle();
 
  return data?.role || "user";
};

フロントエンドでUserテーブルまたは他のデータテーブルから重要なユーザーデータが必要な場合は、fetchUserRoleメソッドで拡張することができます。

5.3 リアルタイムロール更新

テンプレートはロール変更のリアルタイム監視を実装しています:

const userSubscription = supabase
  .channel("public:users")
  .on(
    "postgres_changes",
    {
      event: "UPDATE",
      schema: "public",
      table: "users",
      filter: `id=eq.${user?.id}`,
    },
    async (payload) => {
      if (user) {
        setUser({
          ...user,
          role: payload.new.role,
        });
      }
    }
  )
  .subscribe();

5.4 複数ログイン方法のサポート

ソーシャルログイン

const signInWithGoogle = async () => {
  return await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: `${window.location.origin}/auth/callback?next=${next}`,
    },
  });
};
 
const signInWithGithub = async () => {
  return await supabase.auth.signInWithOAuth({
    provider: "github",
    options: {
      redirectTo: `${window.location.origin}/auth/callback?next=${next}`,
    },
  });
};

メールマジックリンク

const signInWithEmail = async (email: string, captchaToken?: string) => {
  return await supabase.auth.signInWithOtp({
    email: normalizeEmail(email),
    options: {
      emailRedirectTo: `${window.location.origin}/auth/callback?next=${next}`,
      captchaToken,
    },
  });
};

ステップ6:認証コールバックの処理

テンプレートは完全な認証コールバック処理メカニズムを実装しています。

6.1 OAuthコールバック

app/auth/callback/route.ts
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  let next = searchParams.get('next') ?? '/'
 
  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(`${origin}${next}`)
    }
  }
 
  return NextResponse.redirect(new URL(`/redirect-error?code=server_error`, origin))
}

6.2 メール確認

app/auth/confirm/route.ts
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const token_hash = searchParams.get('token_hash')
  const type = searchParams.get('type') as EmailOtpType | null
 
  if (!token_hash || !type) {
    return NextResponse.redirect(new URL(`/redirect-error?code=invalid_params`, origin))
  }
 
  const supabase = await createClient()
  const { error } = await supabase.auth.verifyOtp({
    type,
    token_hash,
  })
 
  if (error) {
    // 詳細なエラー処理
    switch (error.message) {
      case 'Token has expired':
        return NextResponse.redirect(new URL(`/redirect-error?code=token_expired`, origin))
      // ... その他のエラー処理
    }
  }
 
  return NextResponse.redirect(new URL(next, origin))
}

実際のプロジェクトでの応用

Client Componentsでの使用

import { useAuth } from '@/components/providers/AuthProvider';
 
export default function UserProfile() {
  const { user, loading, signOut } = useAuth();
 
  if (loading) return <div>Loading...</div>;
  
  if (!user) {
    return <LoginButton />;
  }
 
  return (
    <div>
      <h1>Welcome, {user.email}</h1>
      <p>Role: {user.role}</p>
      {user.role === 'admin' && <AdminPanel />}
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
}

Server Componentsでの使用

import { createClient } from '@/lib/supabase/server';
import { redirect } from 'next/navigation';
 
export default async function PrivatePage() {
  const supabase = await createClient();
  const { data: { user }, error } = await supabase.auth.getUser();
 
  if (error || !user) {
    redirect('/login');
  }
 
  return <p>Hello {user.email}</p>;
}

Server Actionsでの使用

'use server'
import { createClient as createAdminClient } from "@supabase/supabase-js";
import { isAdmin } from '@/lib/supabase/isAdmin';
 
export async function adminAction() {
  if (!(await isAdmin())) {
    return actionResponse.forbidden("管理者権限が必要です。");
  }
 
  const supabaseAdmin = createAdminClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_ROLE_KEY!
  );
  // 管理者操作を実行
}

セキュリティのベストプラクティス

1. サーバーサイドではgetSession()の代わりにgetUser()を使用

// ✅ 正しい - サーバーサイドでは常にトークンを検証
const { data: { user } } = await supabase.auth.getUser();
 
// ❌ 間違い - サーバーサイドでは使用すべきではない、トークンが検証されない可能性がある
const { data: { session } } = await supabase.auth.getSession();

2. クライアントとサーバーサイドのロジックを分離

// Clientコンポーネント - ブラウザクライアントを使用
const supabase = createClient(); // from @/lib/supabase/client
 
// Serverコンポーネント/Actions - サーバーサイドクライアントを使用  
const supabase = await createClient(); // from @/lib/supabase/server

3. 機密操作では認証後にService Role Keyを使用

機密操作では、まず管理者身元を確認し、その後Service Role Keyを使用してデータベースを操作

import { createClient as createAdminClient } from "@supabase/supabase-js";
import { isAdmin } from '@/lib/supabase/isAdmin';
 
// 管理者身元を確認
if (!(await isAdmin())) {
  return actionResponse.forbidden("管理者権限が必要です。");
}
 
// 管理者はService Role Keyを使用してデータベースを操作
const supabaseAdmin = createAdminClient<Database>(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY! // サーバーサイドのみ
);