إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار

المصادقة هي حارس البوابة لكل تطبيق ويب جاد. سواء كنت تبني لوحة تحكم SaaS أو منصة تجارة إلكترونية أو أداة داخلية، فأنت بحاجة لنظام مصادقة آمن وقابل للصيانة. Auth.js v5 (المعروف سابقاً بـ NextAuth.js) هو المعيار الفعلي للمصادقة في بيئة Next.js — ومع إعادة كتابة الإصدار الخامس، أصبح أسرع وأبسط وأقوى من أي وقت مضى.
في هذا الدليل التعليمي، ستبني نظام مصادقة كاملاً من الصفر يشمل:
- تسجيل الدخول عبر Google OAuth
- المصادقة بالبريد الإلكتروني وكلمة المرور
- إدارة الجلسات باستخدام JWT
- حماية المسارات عبر middleware
- التحكم بالوصول حسب الأدوار (RBAC)
لماذا Auth.js v5؟ يقدم الإصدار الخامس دعماً مدمجاً لـ App Router ومتوافقاً مع Edge Runtime وواجهة برمجية مبسطة وحماية CSRF مدمجة. إذا كنت تبدأ مشروع Next.js جديداً في 2026، فإن Auth.js v5 هو الخيار الموصى به.
المتطلبات المسبقة
قبل البدء، تأكد من توفر:
- Node.js 18+ مثبت على جهازك
- مشروع Next.js 15 (App Router)
- حساب في Google Cloud Console (لـ OAuth)
- معرفة أساسية بـ TypeScript و React Server Components
ما الذي ستبنيه
تطبيق Next.js 15 يتضمن:
- صفحة تسجيل دخول تدعم Google OAuth والبريد الإلكتروني/كلمة المرور
- لوحة تحكم محمية لا يمكن الوصول إليها إلا للمستخدمين المصادق عليهم
- لوحة إدارة مقيدة للمستخدمين ذوي دور
admin - Middleware يعيد توجيه المستخدمين غير المصادق عليهم تلقائياً
الخطوة 1: إعداد مشروع Next.js
إذا لم يكن لديك مشروع بعد، أنشئ واحداً:
npx create-next-app@latest my-auth-app --typescript --tailwind --app --src-dir
cd my-auth-appثبّت الحزم المطلوبة:
npm install next-auth@beta @auth/prisma-adapter @prisma/client prisma bcryptjs zod
npm install -D @types/bcryptjsوظيفة كل حزمة:
| الحزمة | الغرض |
|---|---|
next-auth@beta | Auth.js v5 لـ Next.js |
@auth/prisma-adapter | حفظ المستخدمين والجلسات في قاعدة البيانات |
prisma | ORM لقاعدة البيانات |
bcryptjs | تشفير كلمات المرور |
zod | التحقق من صحة البيانات |
الخطوة 2: إعداد Prisma
هيئ Prisma مع قاعدة البيانات المفضلة لديك (سنستخدم PostgreSQL):
npx prisma init --datasource-provider postgresqlحدّث ملف prisma/schema.prisma بنماذج Auth.js:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
passwordHash String?
role String @default("user")
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}لاحظ حقلي passwordHash و role في نموذج User — هذه إضافات مخصصة لتسجيل الدخول بالبيانات الاعتمادية والتحكم بالأدوار.
شغّل عملية الترحيل:
npx prisma migrate dev --name init
npx prisma generateأنشئ عميل Prisma في src/lib/prisma.ts:
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prismaالخطوة 3: إعداد Auth.js
أنشئ ملف الإعداد الرئيسي لـ Auth.js في src/auth.ts:
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
import { z } from "zod"
const signInSchema = z.object({
email: z.string().email("عنوان بريد إلكتروني غير صالح"),
password: z.string().min(8, "كلمة المرور يجب أن تكون 8 أحرف على الأقل"),
})
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
role: "user",
}
},
}),
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const parsed = signInSchema.safeParse(credentials)
if (!parsed.success) return null
const { email, password } = parsed.data
const user = await prisma.user.findUnique({
where: { email },
})
if (!user || !user.passwordHash) return null
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) return null
return {
id: user.id,
name: user.name,
email: user.email,
image: user.image,
role: user.role,
}
},
}),
],
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60,
},
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = (user as any).role ?? "user"
}
return token
},
session({ session, token }) {
if (session.user) {
session.user.id = token.id as string
;(session.user as any).role = token.role as string
}
return session
},
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user
const isOnDashboard = nextUrl.pathname.startsWith("/dashboard")
const isOnAdmin = nextUrl.pathname.startsWith("/admin")
if (isOnDashboard || isOnAdmin) {
if (isLoggedIn) return true
return false
}
return true
},
},
})ما يحدث هنا:
- موفر Google يتعامل مع OAuth مع تعيين تلقائي للملف الشخصي
- موفر البيانات الاعتمادية يتحقق من البريد/كلمة المرور باستخدام Zod و bcrypt
- استدعاءات JWT تحفظ دور المستخدم في الرمز المميز
- استدعاء الجلسة يكشف الدور لمكونات العميل
- استدعاء التفويض يحمي مسارات
/dashboardو/admin
الخطوة 4: إعداد مسار API
أنشئ مسار API لـ Auth.js في src/app/api/auth/[...nextauth]/route.ts:
import { handlers } from "@/auth"
export const { GET, POST } = handlersهذا كل شيء — Auth.js v5 يجعل هذا نظيفاً للغاية.
الخطوة 5: إضافة Middleware لحماية المسارات
أنشئ src/middleware.ts:
export { auth as default } from "@/auth"
export const config = {
matcher: [
"/dashboard/:path*",
"/admin/:path*",
"/api/protected/:path*",
],
}هذا الـ middleware يشغّل استدعاء authorized من إعدادات Auth.js على كل مسار مطابق. المستخدمون غير المصادق عليهم يُعاد توجيههم تلقائياً لصفحة تسجيل الدخول.
الخطوة 6: توسيع أنواع TypeScript
للحصول على تصنيف صحيح لـ session.user.role، أنشئ src/types/next-auth.d.ts:
import { DefaultSession } from "next-auth"
declare module "next-auth" {
interface Session {
user: {
id: string
role: string
} & DefaultSession["user"]
}
interface User {
role?: string
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string
role: string
}
}الخطوة 7: بناء صفحة تسجيل الدخول
أنشئ صفحة تسجيل دخول مخصصة في src/app/auth/signin/page.tsx:
import { signIn, auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function SignInPage() {
const session = await auth()
if (session) redirect("/dashboard")
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">
<div className="text-center">
<h1 className="text-3xl font-bold text-gray-900">مرحباً بعودتك</h1>
<p className="mt-2 text-gray-600">سجّل الدخول إلى حسابك</p>
</div>
{/* Google OAuth */}
<form
action={async () => {
"use server"
await signIn("google", { redirectTo: "/dashboard" })
}}
>
<button
type="submit"
className="w-full flex items-center justify-center gap-3 px-4 py-3 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
المتابعة مع Google
</button>
</form>
<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={async (formData) => {
"use server"
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirectTo: "/dashboard",
})
}}
className="space-y-4"
>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
البريد الإلكتروني
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="you@example.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
كلمة المرور
</label>
<input
id="password"
name="password"
type="password"
required
minLength={8}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<button
type="submit"
className="w-full py-3 px-4 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors"
>
تسجيل الدخول
</button>
</form>
<p className="text-center text-sm text-gray-600">
ليس لديك حساب؟{" "}
<a href="/auth/register" className="text-blue-600 hover:underline">
أنشئ حساباً
</a>
</p>
</div>
</div>
)
}الخطوة 8: إنشاء نقطة نهاية التسجيل
أنشئ server action لتسجيل المستخدم في src/app/auth/register/actions.ts:
"use server"
import { prisma } from "@/lib/prisma"
import bcrypt from "bcryptjs"
import { z } from "zod"
const registerSchema = z.object({
name: z.string().min(2, "الاسم يجب أن يكون حرفين على الأقل"),
email: z.string().email("عنوان بريد إلكتروني غير صالح"),
password: z.string().min(8, "كلمة المرور يجب أن تكون 8 أحرف على الأقل"),
})
export async function register(formData: FormData) {
const parsed = registerSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
password: formData.get("password"),
})
if (!parsed.success) {
return { error: parsed.error.errors[0].message }
}
const { name, email, password } = parsed.data
const existingUser = await prisma.user.findUnique({
where: { email },
})
if (existingUser) {
return { error: "يوجد حساب بهذا البريد الإلكتروني بالفعل" }
}
const passwordHash = await bcrypt.hash(password, 12)
await prisma.user.create({
data: {
name,
email,
passwordHash,
role: "user",
},
})
return { success: true }
}الخطوة 9: بناء لوحة تحكم محمية
أنشئ لوحة تحكم في src/app/dashboard/page.tsx:
import { auth, signOut } from "@/auth"
import { redirect } from "next/navigation"
export default async function DashboardPage() {
const session = await auth()
if (!session?.user) redirect("/auth/signin")
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-4xl mx-auto">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-gray-900">لوحة التحكم</h1>
<form
action={async () => {
"use server"
await signOut({ redirectTo: "/" })
}}
>
<button
type="submit"
className="px-4 py-2 text-sm text-red-600 border border-red-300 rounded-lg hover:bg-red-50 transition-colors"
>
تسجيل الخروج
</button>
</form>
</div>
<div className="bg-white rounded-xl shadow-sm 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>
<h2 className="text-xl font-semibold">{session.user.name}</h2>
<p className="text-gray-600">{session.user.email}</p>
<span className="inline-block mt-1 px-2 py-0.5 text-xs font-medium bg-blue-100 text-blue-800 rounded-full">
{(session.user as any).role}
</span>
</div>
</div>
</div>
</div>
</div>
)
}الخطوة 10: تطبيق التحكم بالوصول حسب الأدوار
أنشئ صفحة خاصة بالمسؤول في src/app/admin/page.tsx:
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export default async function AdminPage() {
const session = await auth()
if (!session?.user) redirect("/auth/signin")
if ((session.user as any).role !== "admin") redirect("/dashboard")
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-4xl mx-auto">
<div className="flex items-center gap-3 mb-8">
<span className="px-3 py-1 text-sm font-medium bg-red-100 text-red-800 rounded-full">
المسؤولون فقط
</span>
<h1 className="text-3xl font-bold text-gray-900">لوحة الإدارة</h1>
</div>
<div className="bg-white rounded-xl shadow-sm p-6">
<h2 className="text-lg font-semibold mb-4">إدارة المستخدمين</h2>
<p className="text-gray-600">
هذه الصفحة متاحة فقط للمستخدمين الذين يحملون دور المسؤول.
يمكنك إضافة وظائف إدارة المستخدمين هنا.
</p>
</div>
</div>
</div>
)
}لفرض هذا على مستوى الـ middleware أيضاً، حدّث استدعاء authorized في src/auth.ts:
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user
const isOnAdmin = nextUrl.pathname.startsWith("/admin")
if (isOnAdmin) {
if (!isLoggedIn) return false
if ((auth?.user as any)?.role !== "admin") {
return Response.redirect(new URL("/dashboard", nextUrl))
}
return true
}
if (nextUrl.pathname.startsWith("/dashboard")) {
return isLoggedIn
}
return true
},الخطوة 11: إعداد متغيرات البيئة
أنشئ ملف .env.local:
# قاعدة البيانات
DATABASE_URL="postgresql://user:password@localhost:5432/myauthdb"
# Auth.js
AUTH_SECRET="your-random-secret-here"
AUTH_URL="http://localhost:3000"
# Google OAuth
AUTH_GOOGLE_ID="your-google-client-id"
AUTH_GOOGLE_SECRET="your-google-client-secret"الحصول على بيانات اعتماد Google OAuth
- اذهب إلى Google Cloud Console
- أنشئ مشروعاً جديداً أو اختر مشروعاً موجوداً
- انتقل إلى APIs & Services > Credentials
- انقر Create Credentials > OAuth client ID
- اختر Web application
- أضف رابط إعادة التوجيه:
http://localhost:3000/api/auth/callback/google - انسخ Client ID و Client Secret إلى ملف
.env.local
أنشئ مفتاح auth السري:
npx auth secretاختبار التطبيق
شغّل خادم التطوير:
npm run devاختبر التدفقات التالية:
- Google OAuth: انقر "المتابعة مع Google" في صفحة تسجيل الدخول
- التسجيل: أنشئ حساباً جديداً عبر
/auth/register - تسجيل الدخول بالبيانات الاعتمادية: سجّل الدخول بالبريد وكلمة المرور المسجلين
- المسارات المحمية: جرّب الوصول لـ
/dashboardبدون تسجيل الدخول - وصول المسؤول: غيّر دور مستخدم لـ
adminفي قاعدة البيانات وتحقق من وصول/admin
استكشاف الأخطاء وإصلاحها
خطأ "CSRF token mismatch"
تأكد أن AUTH_URL في .env.local يطابق الرابط الفعلي الذي تصل إليه.
إعادة توجيه Google OAuth لرابط خاطئ
تحقق أن رابط إعادة التوجيه في Google Cloud Console يطابق تماماً: http://localhost:3000/api/auth/callback/google
الجلسة null في مكونات العميل
استخدم مغلف SessionProvider في layout إذا كنت تحتاج الوصول للجلسة في مكونات العميل.
تسجيل الدخول بالبيانات الاعتمادية يرجع null
تأكد أن المستخدم لديه حقل passwordHash. المستخدمون الذين سجلوا عبر Google OAuth لن يكون لديهم كلمة مرور.
الخطوات التالية
بعد أن أصبح لديك أساس مصادقة صلب، فكّر في:
- التحقق من البريد الإلكتروني: استخدم موفر البريد في Auth.js مع Resend أو Nodemailer
- المصادقة الثنائية: أضف 2FA باستخدام TOTP
- موفرون اجتماعيون إضافيون: أضف GitHub أو Discord أو Apple
- تحديد معدل الطلبات: احمِ نقطة نهاية تسجيل الدخول من الهجمات
- سجل المراجعة: تتبع أحداث تسجيل الدخول للمراقبة الأمنية
الخلاصة
لقد بنيت نظام مصادقة جاهزاً للإنتاج باستخدام Auth.js v5 و Next.js 15 يتضمن:
- مصادقة مزدوجة: Google OAuth وبيانات اعتماد البريد الإلكتروني/كلمة المرور
- مستخدمون مخزنون في قاعدة بيانات: Prisma مع PostgreSQL
- جلسات JWT: إدارة جلسات سريعة وبدون حالة
- حماية المسارات: تحكم بالوصول عبر middleware
- RBAC: تفويض حسب الأدوار على مستوى الصفحة والـ middleware
واجهة Auth.js v5 المبسطة تجعل من السهل جداً إضافة مصادقة بمستوى المؤسسات لتطبيقات Next.js. الجمع بين Server Components و Server Actions والـ middleware يمنحك طبقات أمان متعددة دون التضحية بتجربة المطور.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء مشروع SaaS متكامل باستخدام Next.js 15 و Stripe و Auth.js v5
تعلم كيفية بناء تطبيق SaaS جاهز للإنتاج باستخدام Next.js 15 مع نظام اشتراكات Stripe ومصادقة Auth.js v5. يغطي هذا الدليل التفصيلي إعداد المشروع وتسجيل الدخول عبر OAuth وخطط الأسعار ومعالجة Webhooks والمسارات المحمية.

بناء تطبيق فوري مع Supabase و Next.js 15: الدليل الشامل
تعلّم كيفية بناء تطبيق full-stack فوري باستخدام Supabase و Next.js 15 App Router. يغطي هذا الدليل المصادقة وإعداد قاعدة البيانات و Row Level Security والاشتراكات الفورية.

بناء وكيل ذكاء اصطناعي مستقل باستخدام Agentic RAG و Next.js
تعلم كيف تبني وكيل ذكاء اصطناعي يقرر بشكل مستقل متى وكيف يسترجع المعلومات من قواعد البيانات المتجهية. دليل عملي شامل باستخدام Vercel AI SDK و Next.js مع أمثلة قابلة للتنفيذ.