احراز هویت در Next.js App Router با Auth.js v5: راهنمای عملی از صفر تا RBAC

راهنمای عملی احراز هویت در Next.js App Router با Auth.js v5. از پیکربندی اولیه و Middleware تا Server Actions، کنترل دسترسی RBAC، مدیریت نشست JWT و دیتابیسی، و نکات امنیتی ضروری.

مقدمه: چرا احراز هویت در اپلیکیشن‌های مدرن Next.js اینقدر مهمه؟

اگه با Next.js کار کرده باشید، حتماً می‌دونید که احراز هویت (Authentication) یکی از اون بخش‌هایی‌ه که هم خیلی مهمه و هم می‌تونه واقعاً سردرگم‌کننده باشه. حالا با اومدن App Router در Next.js نسخه ۱۳ به بعد، معماری اپلیکیشن‌ها از بنیاد عوض شده. مفاهیم جدیدی مثل Server Components، Server Actions و Middleware پیشرفته وارد بازی شدن که هم فرصت‌های جدیدی ایجاد کردن و هم الگوهای قدیمی رو منسوخ کردن.

Auth.js v5 (که قبلاً NextAuth.js صداش می‌کردیم) به طور کامل بازنویسی شده تا با App Router هماهنگ باشه. صادقانه بگم، تغییراتش اول کمی گیج‌کننده‌ست ولی وقتی باهاش کنار بیاید، واقعاً قدرتمنده.

خب، بیاید شروع کنیم. تو این راهنما قراره از پیکربندی اولیه تا الگوهای پیشرفته مثل RBAC و بهینه‌سازی عملکرد رو با هم بررسی کنیم.

درک معماری چندلایه احراز هویت

یکی از تفاوت‌های اساسی App Router با Pages Router قدیمی، معماری چندلایه‌ای‌ه که برای احراز هویت در اختیارتون قرار می‌ده. این معماری چهار لایه اصلی داره:

۱. لایه Middleware

Middleware اولین خط دفاعی شماست. قبل از رسیدن درخواست به هر صفحه یا API اجرا می‌شه و توی Edge Runtime کار می‌کنه. برای بررسی سریع توکن‌های نشست و هدایت کاربران غیرمجاز، ایده‌آله. می‌تونه درخواست‌ها رو رهگیری کنه، هدرها رو تغییر بده و کاربران رو ریدایرکت کنه.

۲. لایه Server Components

Server Components روی سرور رندر می‌شن و دسترسی مستقیم به نشست کاربر دارن. با تابع auth() می‌تونید اطلاعات نشست رو بگیرید و محتوای صفحه رو بر اساس وضعیت کاربر تنظیم کنید. برای رندر شرطی محتوا عالیه.

۳. لایه Server Actions

Server Actions توابع سمت سروری هستن که مستقیماً از کامپوننت‌های کلاینت فراخوانی می‌شن. تو Auth.js v5، عملیات signIn و signOut به صورت Server Actions پیاده‌سازی شدن. چرا؟ چون منطق حساس احراز هویت هرگز به سمت کلاینت ارسال نمی‌شه و امنیت خیلی بالاتر می‌ره.

۴. لایه Client Components

کامپوننت‌های کلاینت برای رابط کاربری تعاملی استفاده می‌شن. با SessionProvider و هوک useSession می‌تونید وضعیت نشست رو در سمت کلاینت مدیریت کنید. البته یه نکته مهم: اطلاعات حساس رو فقط در سمت کلاینت بررسی نکنید. همیشه یه لایه محافظتی سمت سرور هم داشته باشید.

رویکرد دفاع در عمق (Defense in Depth) یعنی از ترکیب همه این لایه‌ها استفاده کنید. Middleware برای محافظت سریع مسیرها، Server Components برای بررسی دقیق مجوزها، و Server Actions برای عملیات امن — همه باهم کار می‌کنن.

راه‌اندازی Auth.js v5 با Next.js App Router

بیاید از نصب شروع کنیم. Auth.js v5 هنوز تو مرحله بتاست (در زمان نگارش)، پس با این دستور نصبش کنید:

npm install next-auth@beta

بعدش متغیرهای محیطی رو تنظیم کنید. یه تغییر مهم: تو نسخه ۵، پیشوند متغیرها از NEXTAUTH_ به AUTH_ تغییر کرده:

# .env.local

# کلید رمزنگاری نشست‌ها - حتماً یک مقدار تصادفی و امن استفاده کنید
AUTH_SECRET="your-super-secret-key-at-least-32-characters"

# آدرس پایه اپلیکیشن
AUTH_URL="http://localhost:3000"

# اطلاعات OAuth گوگل
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"

# اطلاعات OAuth گیت‌هاب
AUTH_GITHUB_ID="your-github-client-id"
AUTH_GITHUB_SECRET="your-github-client-secret"

حالا وقت ایجاد فایل اصلی پیکربندیه. تو نسخه ۵، همه چیز تو یه فایل auth.ts متمرکز شده و توابع auth، handlers، signIn و signOut ازش اکسپورت می‌شن:

// auth.ts - فایل اصلی پیکربندی احراز هویت

import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import Credentials from "next-auth/providers/credentials"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "@/lib/db"
import bcrypt from "bcryptjs"

export const { handlers, auth, signIn, signOut } = NextAuth({
  // آداپتور پایگاه داده (اختیاری - برای نشست‌های دیتابیسی)
  adapter: DrizzleAdapter(db),

  // تعریف ارائه‌دهندگان احراز هویت
  providers: [
    Google({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Credentials({
      name: "credentials",
      credentials: {
        email: { label: "ایمیل", type: "email" },
        password: { label: "رمز عبور", type: "password" },
      },
      // تابع اعتبارسنجی اطلاعات ورود
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }

        // جستجوی کاربر در پایگاه داده
        const user = await db.query.users.findFirst({
          where: (users, { eq }) => eq(users.email, credentials.email as string),
        })

        if (!user || !user.hashedPassword) {
          return null
        }

        // بررسی صحت رمز عبور
        const isPasswordValid = await bcrypt.compare(
          credentials.password as string,
          user.hashedPassword
        )

        if (!isPasswordValid) {
          return null
        }

        return {
          id: user.id,
          email: user.email,
          name: user.name,
          role: user.role,
          image: user.image,
        }
      },
    }),
  ],

  // تنظیمات نشست
  session: {
    strategy: "jwt", // استفاده از JWT برای مدیریت نشست
    maxAge: 30 * 24 * 60 * 60, // ۳۰ روز
  },

  // صفحات سفارشی
  pages: {
    signIn: "/auth/login",
    error: "/auth/error",
    newUser: "/auth/register",
  },

  // توابع callback برای سفارشی‌سازی رفتار
  callbacks: {
    // افزودن اطلاعات اضافی به توکن JWT
    async jwt({ token, user }) {
      if (user) {
        token.role = user.role
        token.id = user.id
      }
      return token
    },
    // افزودن اطلاعات اضافی به نشست
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role as string
        session.user.id = token.id as string
      }
      return session
    },
    // کنترل مجوز دسترسی
    async authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user
      const isOnDashboard = nextUrl.pathname.startsWith("/dashboard")
      const isOnAuth = nextUrl.pathname.startsWith("/auth")

      if (isOnDashboard) {
        if (isLoggedIn) return true
        return false // ریدایرکت به صفحه ورود
      }

      if (isOnAuth) {
        if (isLoggedIn) {
          return Response.redirect(new URL("/dashboard", nextUrl))
        }
      }

      return true
    },
  },
})

حالا Route Handler رو بسازید:

// app/api/auth/[...nextauth]/route.ts

import { handlers } from "@/auth"

export const { GET, POST } = handlers

همین! این فایل کوچیک تمام مسیرهای API مورد نیاز Auth.js رو مدیریت می‌کنه — از callback و signin گرفته تا signout و session.

محافظت از مسیرها با Middleware

Middleware قدرتمندترین ابزار برای محافظت از مسیرها در سطح Edge هست. فایل middleware.ts باید دقیقاً تو ریشه پروژه (کنار پوشه app) باشه:

// middleware.ts - فایل میدلور در ریشه پروژه

import { auth } from "@/auth"
import { NextResponse } from "next/server"

// مسیرهایی که نیاز به احراز هویت دارند
const protectedRoutes = ["/dashboard", "/profile", "/settings", "/admin"]

// مسیرهای عمومی مربوط به احراز هویت
const authRoutes = ["/auth/login", "/auth/register"]

export default auth((req) => {
  const { nextUrl } = req
  const isLoggedIn = !!req.auth

  // بررسی آیا مسیر فعلی محافظت‌شده است
  const isProtectedRoute = protectedRoutes.some((route) =>
    nextUrl.pathname.startsWith(route)
  )

  // بررسی آیا مسیر فعلی مربوط به احراز هویت است
  const isAuthRoute = authRoutes.some((route) =>
    nextUrl.pathname.startsWith(route)
  )

  // اگر کاربر وارد نشده و مسیر محافظت‌شده است
  if (isProtectedRoute && !isLoggedIn) {
    const redirectUrl = new URL("/auth/login", nextUrl.origin)
    // ذخیره مسیر فعلی برای بازگشت بعد از ورود
    redirectUrl.searchParams.set("callbackUrl", nextUrl.pathname)
    return NextResponse.redirect(redirectUrl)
  }

  // اگر کاربر وارد شده و در صفحه ورود/ثبت‌نام است
  if (isAuthRoute && isLoggedIn) {
    return NextResponse.redirect(new URL("/dashboard", nextUrl.origin))
  }

  // بررسی دسترسی ادمین
  if (nextUrl.pathname.startsWith("/admin")) {
    if (!isLoggedIn || req.auth?.user?.role !== "admin") {
      return NextResponse.redirect(new URL("/unauthorized", nextUrl.origin))
    }
  }

  return NextResponse.next()
})

// تعیین مسیرهایی که میدلور روی آن‌ها اجرا شود
export const config = {
  matcher: [
    /*
     * تطبیق تمام مسیرها به جز:
     * - api (مسیرهای API)
     * - _next/static (فایل‌های استاتیک)
     * - _next/image (بهینه‌سازی تصاویر)
     * - favicon.ico (آیکون سایت)
     * - فایل‌های عمومی با پسوند مشخص
     */
    "/((?!api|_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
  ],
}

یه نکته خیلی مهم: Middleware تو Edge Runtime اجرا می‌شه. یعنی محدودیت‌هایی تو استفاده از Node.js API داره. عملیات سنگین دیتابیس رو اینجا انجام ندید — بررسی سبک توکن JWT بهترین کاری‌ه که می‌تونید توی Middleware بکنید.

الگوی Matcher پیشرفته

می‌تونید از الگوهای دقیق‌تری هم استفاده کنید:

// الگوی matcher پیشرفته برای سناریوهای مختلف
export const config = {
  matcher: [
    // تمام مسیرهای داشبورد
    "/dashboard/:path*",
    // تمام مسیرهای پروفایل
    "/profile/:path*",
    // مسیرهای API خاص (نه همه)
    "/api/protected/:path*",
    // مسیرهای ادمین
    "/admin/:path*",
  ],
}

الگوهای احراز هویت در Server Components

یکی از بهترین چیزهای Auth.js v5 اینه که تابع auth() مستقیماً تو Server Components کار می‌کنه. یه تابع async که نشست فعلی کاربر رو برمی‌گردونه:

// app/dashboard/page.tsx - صفحه داشبورد با بررسی احراز هویت

import { auth } from "@/auth"
import { redirect } from "next/navigation"

export default async function DashboardPage() {
  // دریافت نشست کاربر - مستقیماً در Server Component
  const session = await auth()

  // اگر کاربر وارد نشده باشد، ریدایرکت کن
  if (!session?.user) {
    redirect("/auth/login")
  }

  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">
        خوش آمدید، {session.user.name}!
      </h1>
      <div className="bg-white rounded-lg shadow p-6">
        <div className="flex items-center gap-4">
          {session.user.image && (
            <img
              src={session.user.image}
              alt="تصویر پروفایل"
              className="w-16 h-16 rounded-full"
            />
          )}
          <div>
            <p className="font-semibold">{session.user.name}</p>
            <p className="text-gray-600">{session.user.email}</p>
            <p className="text-sm text-gray-500">
              نقش: {session.user.role === "admin" ? "مدیر" : "کاربر"}
            </p>
          </div>
        </div>
      </div>
    </div>
  )
}

کامپوننت هدر با نمایش شرطی

یه الگوی خیلی رایج: نمایش شرطی دکمه‌های ورود/خروج تو هدر سایت.

// components/Header.tsx - کامپوننت هدر با احراز هویت

import { auth } from "@/auth"
import { SignOutButton } from "./SignOutButton"
import Link from "next/link"

export default async function Header() {
  const session = await auth()

  return (
    <header className="bg-white shadow-sm border-b">
      <nav className="container mx-auto px-6 py-4 flex justify-between items-center">
        <Link href="/" className="text-xl font-bold">
          اپلیکیشن من
        </Link>

        <div className="flex items-center gap-4">
          {session?.user ? (
            <>
              <span className="text-gray-700">
                {session.user.name}
              </span>
              <Link
                href="/dashboard"
                className="text-blue-600 hover:underline"
              >
                داشبورد
              </Link>
              <SignOutButton />
            </>
          ) : (
            <>
              <Link
                href="/auth/login"
                className="px-4 py-2 text-blue-600 hover:underline"
              >
                ورود
              </Link>
              <Link
                href="/auth/register"
                className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
              >
                ثبت‌نام
              </Link>
            </>
          )}
        </div>
      </nav>
    </header>
  )
}

Server Actions برای ورود و خروج

تو Auth.js v5 عملیات ورود و خروج با Server Actions انجام می‌شه. این یعنی اطلاعات حساس هیچ‌وقت به سمت کلاینت نمی‌ره. از نظر من، این یکی از بهترین تصمیمات طراحی Auth.js v5 بوده.

Server Action برای ورود با ایمیل و رمز عبور

// app/actions/auth.ts - اکشن‌های سرور برای احراز هویت
"use server"

import { signIn, signOut } from "@/auth"
import { AuthError } from "next-auth"
import { redirect } from "next/navigation"
import { z } from "zod"

// اسکیمای اعتبارسنجی فرم ورود
const loginSchema = z.object({
  email: z.string().email("ایمیل معتبر وارد کنید"),
  password: z.string().min(8, "رمز عبور باید حداقل ۸ کاراکتر باشد"),
})

// اکشن ورود با اطلاعات کاربری
export async function loginAction(prevState: any, formData: FormData) {
  try {
    // اعتبارسنجی ورودی‌ها
    const validatedFields = loginSchema.safeParse({
      email: formData.get("email"),
      password: formData.get("password"),
    })

    if (!validatedFields.success) {
      return {
        error: "اطلاعات وارد شده معتبر نیست",
        errors: validatedFields.error.flatten().fieldErrors,
      }
    }

    // تلاش برای ورود
    await signIn("credentials", {
      email: validatedFields.data.email,
      password: validatedFields.data.password,
      redirectTo: "/dashboard",
    })
  } catch (error) {
    if (error instanceof AuthError) {
      switch (error.type) {
        case "CredentialsSignin":
          return { error: "ایمیل یا رمز عبور اشتباه است" }
        case "AccessDenied":
          return { error: "دسترسی شما محدود شده است" }
        default:
          return { error: "خطایی در فرآیند ورود رخ داد" }
      }
    }
    throw error // ریدایرکت‌ها باید دوباره پرتاب شوند
  }
}

// اکشن ورود با OAuth
export async function oauthLoginAction(provider: string) {
  await signIn(provider, { redirectTo: "/dashboard" })
}

// اکشن خروج
export async function logoutAction() {
  await signOut({ redirectTo: "/" })
}

فرم ورود با استفاده از Server Action

// app/auth/login/page.tsx - صفحه ورود

"use client"

import { useActionState } from "react"
import { loginAction, oauthLoginAction } from "@/app/actions/auth"

export default function LoginPage() {
  const [state, formAction, isPending] = useActionState(loginAction, null)

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
        <h2 className="text-center text-2xl font-bold">
          ورود به حساب کاربری
        </h2>

        {/* نمایش خطا */}
        {state?.error && (
          <div className="bg-red-50 text-red-600 p-3 rounded-lg text-sm">
            {state.error}
          </div>
        )}

        {/* فرم ورود */}
        <form action={formAction} className="space-y-6">
          <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-lg border border-gray-300 px-3 py-2"
              placeholder="[email protected]"
              dir="ltr"
            />
          </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-lg border border-gray-300 px-3 py-2"
              dir="ltr"
            />
          </div>

          <button
            type="submit"
            disabled={isPending}
            className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50"
          >
            {isPending ? "در حال ورود..." : "ورود"}
          </button>
        </form>

        {/* دکمه‌های ورود با OAuth */}
        <div className="space-y-3">
          <div className="relative">
            <div className="absolute inset-0 flex items-center">
              <div className="w-full border-t border-gray-300" />
            </div>
            <div className="relative flex justify-center text-sm">
              <span className="px-2 bg-white text-gray-500">یا</span>
            </div>
          </div>

          <form action={() => oauthLoginAction("google")}>
            <button
              type="submit"
              className="w-full border border-gray-300 py-2 rounded-lg hover:bg-gray-50"
            >
              ورود با گوگل
            </button>
          </form>

          <form action={() => oauthLoginAction("github")}>
            <button
              type="submit"
              className="w-full border border-gray-300 py-2 rounded-lg hover:bg-gray-50"
            >
              ورود با گیت‌هاب
            </button>
          </form>
        </div>
      </div>
    </div>
  )
}

دکمه خروج به صورت کامپوننت جداگانه

// components/SignOutButton.tsx - دکمه خروج

"use client"

import { logoutAction } from "@/app/actions/auth"

export function SignOutButton() {
  return (
    <form action={logoutAction}>
      <button
        type="submit"
        className="px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg transition"
      >
        خروج
      </button>
    </form>
  )
}

استراتژی‌های مدیریت نشست: JWT در مقابل دیتابیس

Auth.js v5 دو استراتژی اصلی برای مدیریت نشست داره. انتخاب بینشون بستگی به نیاز پروژتون داره.

استراتژی JWT (پیش‌فرض)

تو این روش، اطلاعات نشست تو یه توکن JWT رمزنگاری‌شده ذخیره می‌شه که توی کوکی مرورگر نگهداری می‌شه. بدون نیاز به پایگاه داده کار می‌کنه و سریع‌ترین گزینه‌ست.

  • مزایا: سریع، بدون نیاز به دیتابیس، عالی برای Serverless و Edge
  • معایب: نمی‌تونید نشست رو فوری باطل کنید، اندازه کوکی محدوده، به‌روزرسانی اطلاعات کاربر نیاز به ورود مجدد داره
// پیکربندی نشست JWT
export const { handlers, auth, signIn, signOut } = NextAuth({
  // ...
  session: {
    strategy: "jwt",
    maxAge: 30 * 24 * 60 * 60, // ۳۰ روز اعتبار
    updateAge: 24 * 60 * 60, // به‌روزرسانی هر ۲۴ ساعت
  },
  jwt: {
    maxAge: 30 * 24 * 60 * 60,
  },
  callbacks: {
    async jwt({ token, user, trigger, session }) {
      // هنگام ورود اولیه، اطلاعات کاربر به توکن اضافه شود
      if (user) {
        token.id = user.id
        token.role = user.role
        token.permissions = user.permissions
      }

      // هنگام به‌روزرسانی نشست از سمت کلاینت
      if (trigger === "update" && session) {
        token.name = session.name
      }

      return token
    },
    async session({ session, token }) {
      // اطلاعات توکن به نشست منتقل شود
      session.user.id = token.id as string
      session.user.role = token.role as string
      session.user.permissions = token.permissions as string[]
      return session
    },
  },
})

استراتژی نشست دیتابیسی

تو این روش، نشست‌ها توی دیتابیس ذخیره می‌شن و فقط یه شناسه نشست تو کوکی می‌مونه. امنیتش بالاتره چون هر لحظه می‌تونید نشست رو باطل کنید.

  • مزایا: باطل کردن فوری نشست، ذخیره اطلاعات بیشتر، مدیریت بهتر نشست‌های همزمان
  • معایب: نیاز به دیتابیس، هر درخواست یه کوئری می‌زنه، کمی کندتره
// پیکربندی نشست دیتابیسی
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "@/lib/db"

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: DrizzleAdapter(db),
  session: {
    strategy: "database",
    maxAge: 30 * 24 * 60 * 60,
    updateAge: 24 * 60 * 60,
  },
  // ...
})

توصیه عملی: برای اکثر پروژه‌ها JWT کافیه. ولی اگه اپلیکیشن حساسی دارید (مثل بانکداری آنلاین) که نیاز به باطل کردن فوری نشست دارید، برید سراغ دیتابیسی. یه راه‌حل میانی هم هست: JWT برای سرعت و یه لیست سیاه توی Redis برای باطل کردن نشست‌ها.

الگوهای کنترل دسترسی مبتنی بر نقش (RBAC)

تقریباً هر اپلیکیشن حرفه‌ای نیاز به RBAC (کنترل دسترسی مبتنی بر نقش) داره. بیاید ببینیم چطور با Auth.js v5 می‌تونیم یه سیستم RBAC کامل بسازیم.

تعریف تایپ‌های نقش و مجوز

// types/auth.ts - تعریف تایپ‌ها

// نقش‌های سیستم
export type UserRole = "user" | "editor" | "admin" | "super_admin"

// مجوزهای سیستم
export type Permission =
  | "read:posts"
  | "write:posts"
  | "delete:posts"
  | "manage:users"
  | "manage:settings"
  | "view:analytics"

// نگاشت نقش‌ها به مجوزها
export const rolePermissions: Record<UserRole, Permission[]> = {
  user: ["read:posts"],
  editor: ["read:posts", "write:posts", "delete:posts"],
  admin: [
    "read:posts",
    "write:posts",
    "delete:posts",
    "manage:users",
    "view:analytics",
  ],
  super_admin: [
    "read:posts",
    "write:posts",
    "delete:posts",
    "manage:users",
    "manage:settings",
    "view:analytics",
  ],
}

// بررسی مجوز
export function hasPermission(
  role: UserRole,
  permission: Permission
): boolean {
  return rolePermissions[role]?.includes(permission) ?? false
}

// گسترش تایپ‌های Auth.js
declare module "next-auth" {
  interface User {
    role: UserRole
  }
  interface Session {
    user: {
      id: string
      role: UserRole
      permissions: Permission[]
    } & DefaultSession["user"]
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    role: UserRole
    permissions: Permission[]
  }
}

کامپوننت محافظ دسترسی

این کامپوننت رو خیلی دوست دارم چون خیلی تمیز و قابل استفاده مجدده:

// components/AccessGuard.tsx - کامپوننت محافظ دسترسی

import { auth } from "@/auth"
import { redirect } from "next/navigation"
import { Permission, hasPermission, UserRole } from "@/types/auth"

interface AccessGuardProps {
  children: React.ReactNode
  requiredPermission?: Permission
  requiredRole?: UserRole
  fallback?: React.ReactNode
}

export default async function AccessGuard({
  children,
  requiredPermission,
  requiredRole,
  fallback,
}: AccessGuardProps) {
  const session = await auth()

  // بررسی ورود کاربر
  if (!session?.user) {
    redirect("/auth/login")
  }

  const userRole = session.user.role as UserRole

  // بررسی نقش مورد نیاز
  if (requiredRole) {
    const roleHierarchy: UserRole[] = ["user", "editor", "admin", "super_admin"]
    const userRoleIndex = roleHierarchy.indexOf(userRole)
    const requiredRoleIndex = roleHierarchy.indexOf(requiredRole)

    if (userRoleIndex < requiredRoleIndex) {
      return fallback || (
        <div className="p-8 text-center">
          <h2 className="text-xl font-bold text-red-600">دسترسی محدود</h2>
          <p className="mt-2 text-gray-600">
            شما مجوز دسترسی به این بخش را ندارید.
          </p>
        </div>
      )
    }
  }

  // بررسی مجوز مورد نیاز
  if (requiredPermission && !hasPermission(userRole, requiredPermission)) {
    return fallback || (
      <div className="p-8 text-center">
        <h2 className="text-xl font-bold text-red-600">دسترسی محدود</h2>
        <p className="mt-2 text-gray-600">
          شما مجوز لازم برای انجام این عملیات را ندارید.
        </p>
      </div>
    )
  }

  return <>{children}</>
}

استفاده از کامپوننت محافظ در صفحات

// app/admin/users/page.tsx - صفحه مدیریت کاربران (فقط ادمین)

import AccessGuard from "@/components/AccessGuard"
import UserManagement from "@/components/UserManagement"

export default function AdminUsersPage() {
  return (
    <AccessGuard requiredPermission="manage:users" requiredRole="admin">
      <div className="container mx-auto p-6">
        <h1 className="text-2xl font-bold mb-6">مدیریت کاربران</h1>
        <UserManagement />
      </div>
    </AccessGuard>
  )
}

محافظت از Server Actions با RBAC

// lib/auth-guard.ts - تابع کمکی برای محافظت از اکشن‌ها
"use server"

import { auth } from "@/auth"
import { Permission, hasPermission, UserRole } from "@/types/auth"

// تابع کمکی برای بررسی احراز هویت و مجوزها در Server Actions
export async function requireAuth() {
  const session = await auth()
  if (!session?.user) {
    throw new Error("احراز هویت نشده‌اید. لطفاً وارد شوید.")
  }
  return session
}

export async function requirePermission(permission: Permission) {
  const session = await requireAuth()
  const userRole = session.user.role as UserRole

  if (!hasPermission(userRole, permission)) {
    throw new Error(`شما مجوز «${permission}» را ندارید.`)
  }

  return session
}

// مثال: Server Action محافظت‌شده
export async function deletePostAction(postId: string) {
  // بررسی مجوز حذف پست
  const session = await requirePermission("delete:posts")

  // حذف پست از پایگاه داده
  await db.delete(posts).where(eq(posts.id, postId))

  // ثبت لاگ عملیات
  await db.insert(auditLogs).values({
    userId: session.user.id,
    action: "delete_post",
    targetId: postId,
    timestamp: new Date(),
  })

  revalidatePath("/dashboard/posts")
  return { success: true }
}

یکپارچه‌سازی ارائه‌دهندگان OAuth

Auth.js v5 از ده‌ها ارائه‌دهنده OAuth پشتیبانی می‌کنه. بیاید Google و GitHub رو (که پرکاربردترین‌ها هستن) با هم ببینیم.

پیکربندی Google OAuth

اول برید توی Google Cloud Console یه پروژه بسازید و OAuth 2.0 Client ID بگیرید. آدرس callback رو هم اینطوری بذارید:

# آدرس callback برای Google OAuth
http://localhost:3000/api/auth/callback/google
// پیکربندی Google با دامنه‌های سفارشی
import Google from "next-auth/providers/google"

Google({
  clientId: process.env.AUTH_GOOGLE_ID,
  clientSecret: process.env.AUTH_GOOGLE_SECRET,
  authorization: {
    params: {
      // درخواست دسترسی به پروفایل و ایمیل
      scope: "openid email profile",
      // نمایش صفحه انتخاب حساب هر بار
      prompt: "select_account",
    },
  },
  // سفارشی‌سازی پروفایل کاربر
  profile(profile) {
    return {
      id: profile.sub,
      name: profile.name,
      email: profile.email,
      image: profile.picture,
      role: "user", // نقش پیش‌فرض برای کاربران جدید
    }
  },
})

پیکربندی GitHub OAuth

برای GitHub هم برید تو Settings > Developer settings > OAuth Apps و یه اپلیکیشن جدید بسازید:

// پیکربندی GitHub با دامنه‌های سفارشی
import GitHub from "next-auth/providers/github"

GitHub({
  clientId: process.env.AUTH_GITHUB_ID,
  clientSecret: process.env.AUTH_GITHUB_SECRET,
  // دریافت اطلاعات بیشتر از GitHub
  profile(profile) {
    return {
      id: String(profile.id),
      name: profile.name || profile.login,
      email: profile.email,
      image: profile.avatar_url,
      role: "user",
    }
  },
})

مدیریت لینک کردن حساب‌ها

وقتی یه کاربر با چند ارائه‌دهنده مختلف وارد می‌شه، حساب‌ها باید درست لینک بشن. Auth.js v5 این کار رو از طریق callbackها و eventها مدیریت می‌کنه:

// مدیریت لینک حساب‌ها در auth.ts
export const { handlers, auth, signIn, signOut } = NextAuth({
  // ...
  callbacks: {
    async signIn({ user, account, profile }) {
      // بررسی آیا ایمیل تأیید شده است (برای Google)
      if (account?.provider === "google") {
        return profile?.email_verified === true
      }

      // بررسی آیا حساب GitHub ایمیل معتبر دارد
      if (account?.provider === "github") {
        return !!user.email
      }

      return true
    },
  },
  events: {
    // رویداد پس از لینک شدن حساب جدید
    async linkAccount({ user, account }) {
      console.log(
        `حساب ${account.provider} به کاربر ${user.id} لینک شد`
      )
    },
  },
})

بهترین شیوه‌های امنیتی و دفاع در عمق

امنیت احراز هویت فقط به درست تنظیم کردن یه کتابخونه محدود نمی‌شه. باید یه رویکرد دفاع در عمق داشته باشید — یعنی چندین لایه محافظتی روی هم.

۱. اعتبارسنجی ورودی‌ها

همیشه ورودی‌های کاربر رو سمت سرور اعتبارسنجی کنید. هرگز (تأکید می‌کنم هرگز!) فقط به اعتبارسنجی سمت کلاینت اعتماد نکنید:

// اعتبارسنجی ورودی‌ها با Zod
import { z } from "zod"

const registerSchema = z.object({
  name: z
    .string()
    .min(2, "نام باید حداقل ۲ کاراکتر باشد")
    .max(50, "نام نباید بیشتر از ۵۰ کاراکتر باشد")
    .regex(/^[\u0600-\u06FFa-zA-Z\s]+$/, "نام فقط شامل حروف باشد"),
  email: z.string().email("ایمیل معتبر وارد کنید"),
  password: z
    .string()
    .min(8, "رمز عبور باید حداقل ۸ کاراکتر باشد")
    .regex(/[A-Z]/, "رمز عبور باید شامل حداقل یک حرف بزرگ باشد")
    .regex(/[0-9]/, "رمز عبور باید شامل حداقل یک عدد باشد")
    .regex(/[^A-Za-z0-9]/, "رمز عبور باید شامل حداقل یک کاراکتر خاص باشد"),
})

۲. محدودسازی نرخ درخواست (Rate Limiting)

حملات Brute Force واقعی هستن. حتماً Rate Limiting رو روی مسیرهای احراز هویت بذارید:

// lib/rate-limit.ts - پیاده‌سازی ساده Rate Limiting

const rateLimitMap = new Map<string, { count: number; resetTime: number }>()

export function rateLimit(
  identifier: string,
  maxAttempts: number = 5,
  windowMs: number = 15 * 60 * 1000 // ۱۵ دقیقه
): { success: boolean; remainingAttempts: number } {
  const now = Date.now()
  const record = rateLimitMap.get(identifier)

  // اگر رکورد قبلی منقضی شده، ریست کن
  if (!record || now > record.resetTime) {
    rateLimitMap.set(identifier, {
      count: 1,
      resetTime: now + windowMs,
    })
    return { success: true, remainingAttempts: maxAttempts - 1 }
  }

  // بررسی تعداد تلاش‌ها
  if (record.count >= maxAttempts) {
    return { success: false, remainingAttempts: 0 }
  }

  record.count++
  return { success: true, remainingAttempts: maxAttempts - record.count }
}

// استفاده در Server Action
export async function loginAction(prevState: any, formData: FormData) {
  const email = formData.get("email") as string

  // بررسی محدودیت نرخ
  const { success, remainingAttempts } = rateLimit(email)
  if (!success) {
    return {
      error: "تعداد تلاش‌های ورود بیش از حد مجاز است. لطفاً ۱۵ دقیقه صبر کنید.",
    }
  }

  // ادامه فرآیند ورود...
}

۳. محافظت در برابر CSRF

Auth.js v5 خودش از توکن‌های CSRF برای فرم‌ها استفاده می‌کنه. ولی وقتی API Route سفارشی می‌سازید، خودتون باید مراقب باشید:

// app/api/protected/route.ts - مسیر API محافظت‌شده

import { auth } from "@/auth"
import { NextRequest, NextResponse } from "next/server"

export async function GET(request: NextRequest) {
  // بررسی احراز هویت
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "احراز هویت نشده‌اید" },
      { status: 401 }
    )
  }

  // بررسی نقش کاربر
  if (session.user.role !== "admin") {
    return NextResponse.json(
      { error: "دسترسی غیرمجاز" },
      { status: 403 }
    )
  }

  // پردازش درخواست
  return NextResponse.json({
    message: "اطلاعات محرمانه",
    user: session.user,
  })
}

export async function POST(request: NextRequest) {
  const session = await auth()

  if (!session?.user) {
    return NextResponse.json(
      { error: "احراز هویت نشده‌اید" },
      { status: 401 }
    )
  }

  try {
    const body = await request.json()
    // اعتبارسنجی و پردازش داده‌ها
    // ...

    return NextResponse.json({ success: true })
  } catch (error) {
    return NextResponse.json(
      { error: "خطا در پردازش درخواست" },
      { status: 400 }
    )
  }
}

۴. تنظیمات امن کوکی

Auth.js v5 خودش تنظیمات امن کوکی رو اعمال می‌کنه، ولی اگه بخواید سفارشی‌سازی کنید:

// تنظیمات سفارشی کوکی در auth.ts
export const { handlers, auth, signIn, signOut } = NextAuth({
  // ...
  cookies: {
    sessionToken: {
      name: "__Secure-authjs.session-token",
      options: {
        httpOnly: true,  // غیرقابل دسترسی از JavaScript
        sameSite: "lax", // محافظت در برابر CSRF
        path: "/",
        secure: process.env.NODE_ENV === "production", // فقط HTTPS در محیط تولید
      },
    },
  },
  // فعال‌سازی trustHost برای محیط‌های پشت پروکسی
  trustHost: true,
})

۵. ثبت لاگ‌های امنیتی

این بخش رو خیلی‌ها نادیده می‌گیرن، ولی ثبت رویدادهای احراز هویت برای شناسایی فعالیت‌های مشکوک ضروریه:

// ثبت رویدادهای امنیتی
export const { handlers, auth, signIn, signOut } = NextAuth({
  // ...
  events: {
    async signIn({ user, account }) {
      console.log(`[امنیت] ورود: کاربر ${user.email} از طریق ${account?.provider}`)
      // ذخیره در پایگاه داده لاگ
      await db.insert(securityLogs).values({
        userId: user.id!,
        event: "sign_in",
        provider: account?.provider || "unknown",
        timestamp: new Date(),
        ipAddress: headers().get("x-forwarded-for") || "unknown",
      })
    },
    async signOut({ token }) {
      console.log(`[امنیت] خروج: کاربر ${token?.email}`)
    },
    async session({ session }) {
      // لاگ دسترسی به نشست (فقط در صورت نیاز)
    },
  },
  // لاگ خطاهای احراز هویت
  logger: {
    error(code, ...message) {
      console.error(`[خطای احراز هویت] کد: ${code}`, message)
    },
    warn(code, ...message) {
      console.warn(`[هشدار احراز هویت] کد: ${code}`, message)
    },
  },
})

بهینه‌سازی عملکرد با Streaming Authentication

یکی از قابلیت‌های فوق‌العاده App Router، Streaming هست. با این قابلیت می‌تونید بخش‌هایی از صفحه رو قبل از آماده شدن تمام داده‌ها نمایش بدید. تأثیرش روی تجربه کاربری واقعاً محسوسه.

استفاده از Suspense با احراز هویت

// app/dashboard/page.tsx - داشبورد با Streaming

import { Suspense } from "react"
import { auth } from "@/auth"
import { redirect } from "next/navigation"

// کامپوننت‌های بارگذاری
function StatsSkeleton() {
  return (
    <div className="grid grid-cols-3 gap-4">
      {[1, 2, 3].map((i) => (
        <div key={i} className="h-32 bg-gray-200 animate-pulse rounded-lg" />
      ))}
    </div>
  )
}

function ActivitySkeleton() {
  return (
    <div className="space-y-3">
      {[1, 2, 3, 4, 5].map((i) => (
        <div key={i} className="h-16 bg-gray-200 animate-pulse rounded-lg" />
      ))}
    </div>
  )
}

// کامپوننت‌هایی که داده‌های سنگین بارگذاری می‌کنند
async function DashboardStats() {
  const session = await auth()
  // دریافت آمار از پایگاه داده
  const stats = await fetchUserStats(session!.user.id)

  return (
    <div className="grid grid-cols-3 gap-4">
      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500">بازدیدها</h3>
        <p className="text-3xl font-bold">{stats.views}</p>
      </div>
      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500">پست‌ها</h3>
        <p className="text-3xl font-bold">{stats.posts}</p>
      </div>
      <div className="bg-white p-6 rounded-lg shadow">
        <h3 className="text-gray-500">نظرات</h3>
        <p className="text-3xl font-bold">{stats.comments}</p>
      </div>
    </div>
  )
}

async function RecentActivity() {
  const session = await auth()
  const activities = await fetchRecentActivities(session!.user.id)

  return (
    <div className="space-y-3">
      {activities.map((activity) => (
        <div key={activity.id} className="bg-white p-4 rounded-lg shadow">
          <p className="font-medium">{activity.title}</p>
          <p className="text-sm text-gray-500">{activity.date}</p>
        </div>
      ))}
    </div>
  )
}

// صفحه اصلی داشبورد با Streaming
export default async function DashboardPage() {
  const session = await auth()

  if (!session?.user) {
    redirect("/auth/login")
  }

  return (
    <div className="container mx-auto p-6 space-y-8">
      <h1 className="text-2xl font-bold">
        سلام {session.user.name}!
      </h1>

      {/* آمار با Streaming - نمایش اسکلتون تا آماده شدن داده */}
      <section>
        <h2 className="text-xl font-semibold mb-4">آمار کلی</h2>
        <Suspense fallback={<StatsSkeleton />}>
          <DashboardStats />
        </Suspense>
      </section>

      {/* فعالیت‌های اخیر با Streaming */}
      <section>
        <h2 className="text-xl font-semibold mb-4">فعالیت‌های اخیر</h2>
        <Suspense fallback={<ActivitySkeleton />}>
          <RecentActivity />
        </Suspense>
      </section>
    </div>
  )
}

کش کردن نشست‌ها برای عملکرد بهتر

اگه توی یه صفحه چندین Server Component دارید که همه‌شون نیاز به نشست دارن، نمی‌خواید هر بار auth() رو صدا بزنید. با React.cache مشکل حل می‌شه:

// lib/get-session.ts - کش کردن نشست در هر درخواست

import { cache } from "react"
import { auth } from "@/auth"

// تابع auth در هر درخواست فقط یک بار اجرا می‌شود
export const getSession = cache(async () => {
  const session = await auth()
  return session
})
// استفاده در چندین کامپوننت - فقط یک بار فراخوانی واقعی
import { getSession } from "@/lib/get-session"

// کامپوننت ۱
async function UserProfile() {
  const session = await getSession() // فراخوانی واقعی
  return <p>{session?.user?.name}</p>
}

// کامپوننت ۲
async function UserRole() {
  const session = await getSession() // از کش خوانده می‌شود
  return <p>نقش: {session?.user?.role}</p>
}

بهینه‌سازی Middleware برای عملکرد

Middleware هر درخواستی رو می‌بینه، پس باید سبک و سریع باشه. کوئری دیتابیس توی Middleware؟ نه لطفاً!

// middleware.ts - بهینه‌سازی شده

import { auth } from "@/auth"

export default auth((req) => {
  // فقط بررسی سریع وجود نشست
  // بدون کوئری پایگاه داده
  // بدون عملیات سنگین

  const isLoggedIn = !!req.auth
  const pathname = req.nextUrl.pathname

  // استفاده از شرایط ساده و سریع
  if (pathname.startsWith("/dashboard") && !isLoggedIn) {
    return Response.redirect(new URL("/auth/login", req.nextUrl.origin))
  }
})

export const config = {
  // فقط مسیرهای لازم - نه همه مسیرها
  matcher: ["/dashboard/:path*", "/admin/:path*", "/profile/:path*"],
}

تکمیل SessionProvider برای کامپوننت‌های کلاینت

اگه نیاز دارید توی کامپوننت‌های کلاینت به اطلاعات نشست دسترسی داشته باشید، باید از SessionProvider استفاده کنید. راه‌اندازیش ساده‌ست:

// app/providers.tsx - تأمین‌کننده نشست برای کامپوننت‌های کلاینت

"use client"

import { SessionProvider } from "next-auth/react"

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <SessionProvider
      // فاصله زمانی بازخوانی نشست (ثانیه)
      refetchInterval={5 * 60}
      // بازخوانی هنگام بازگشت به تب
      refetchOnWindowFocus={true}
    >
      {children}
    </SessionProvider>
  )
}
// app/layout.tsx - لایه‌بندی اصلی

import { Providers } from "./providers"

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="fa" dir="rtl">
      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  )
}
// components/ClientUserMenu.tsx - منوی کاربر در سمت کلاینت

"use client"

import { useSession } from "next-auth/react"

export function ClientUserMenu() {
  const { data: session, status } = useSession()

  if (status === "loading") {
    return <div className="animate-pulse h-8 w-24 bg-gray-200 rounded" />
  }

  if (status === "unauthenticated") {
    return (
      <a href="/auth/login" className="text-blue-600">
        ورود
      </a>
    )
  }

  return (
    <div className="flex items-center gap-2">
      <span>{session?.user?.name}</span>
      {session?.user?.image && (
        <img
          src={session.user.image}
          alt="تصویر پروفایل"
          className="w-8 h-8 rounded-full"
        />
      )}
    </div>
  )
}

جمع‌بندی

خب، خسته نباشید! کلی مسیر رو با هم طی کردیم. احراز هویت تو Next.js App Router با Auth.js v5 واقعاً یه اکوسیستم کامل و منعطف در اختیارتون می‌ذاره.

خلاصه نکات کلیدی:

  1. دفاع در عمق: از ترکیب Middleware، Server Components و Server Actions استفاده کنید. هرگز فقط به یه لایه اعتماد نکنید.
  2. Middleware رو سبک نگه دارید: فقط بررسی سریع توکن، نه کوئری سنگین دیتابیس. بررسی‌های دقیق‌تر مجوزها رو بذارید برای Server Components.
  3. Server Actions برای عملیات حساس: منطق ورود و خروج رو تو Server Actions نگه دارید تا هرگز به کلاینت نره.
  4. انتخاب درست استراتژی نشست: JWT برای اکثر پروژه‌ها خوبه. برای اپلیکیشن‌های حساس، دیتابیسی رو انتخاب کنید.
  5. RBAC رو چندلایه پیاده کنید: بررسی نقش‌ها هم تو Middleware، هم Server Components و هم Server Actions باید باشه.
  6. اعتبارسنجی و Rate Limiting: ورودی‌ها رو همیشه سمت سرور اعتبارسنجی کنید و Rate Limiting رو فراموش نکنید.
  7. بهینه‌سازی عملکرد: از Streaming و Suspense برای تجربه کاربری بهتر و از React.cache برای جلوگیری از فراخوانی‌های تکراری استفاده کنید.
  8. لاگ‌های امنیتی: تمام رویدادهای احراز هویت رو ثبت کنید. روزی ممکنه نجات‌تون بده!

Auth.js v5 با معماری جدیدش، پیاده‌سازی احراز هویت امن و حرفه‌ای رو تو Next.js App Router واقعاً ساده‌تر کرده. با رعایت الگوهایی که تو این مقاله بررسی کردیم، می‌تونید یه سیستم احراز هویت قوی و مقیاس‌پذیر بسازید. فقط یادتون باشه که امنیت یه فرآیند مداومه — همیشه آخرین آپدیت‌ها و آسیب‌پذیری‌ها رو دنبال کنید.

درباره نویسنده Editorial Team

Our team of expert writers and editors.