ทำไมต้อง Auth.js v5 ใน Next.js 16?
Auth.js v5 (เดิมชื่อ NextAuth.js) คือไลบรารี Authentication ที่ถือว่าเป็นมาตรฐานของ Next.js เลยก็ว่าได้ มันถูกเขียนใหม่หมดเพื่อรองรับ App Router อย่างเต็มที่ แต่สิ่งที่ทำให้ Next.js 16 น่าสนใจเป็นพิเศษคือ — middleware.ts ถูกเปลี่ยนชื่อเป็น proxy.ts ซึ่งหมายความว่าการปกป้อง Route ด้วย Auth.js ก็ต้องปรับตามไปด้วย
ตอนแรกที่เห็นก็ตกใจเหมือนกัน แต่พอลองใช้จริงแล้วกลับชอบมากกว่าเดิมอีก
สิ่งที่ Auth.js v5 มอบให้ในปี 2026:
- ฟังก์ชัน
auth()ตัวเดียวใช้ได้ทุกที่ — Server Components, Route Handlers, proxy.ts และ Server Actions - รองรับ Edge Runtime โดยกำเนิดผ่าน Web Standard APIs
- CSRF Protection แบบ Built-in (ไม่ต้องตั้งค่าเพิ่มเอง)
- รองรับ OAuth, Credentials และ Magic Link ในคอนฟิกเดียว
- Type-safe เต็มรูปแบบด้วย TypeScript
ในบทความนี้เราจะพาทำตั้งแต่ติดตั้ง Auth.js v5, สร้าง Credentials Provider สำหรับล็อกอินด้วยอีเมล/รหัสผ่าน, เชื่อมต่อ Google OAuth, ปกป้อง Route ด้วย proxy.ts ตัวใหม่ จัดการ Session ใน Server Components ไปจนถึงสร้างระบบ RBAC แบบ Production-ready เลย
ติดตั้งและตั้งค่าโปรเจกต์
สร้างโปรเจกต์ Next.js 16 ใหม่
npx create-next-app@latest my-auth-app
cd my-auth-app
ตอนที่ CLI ถามให้เลือก App Router, TypeScript และ Tailwind CSS ไว้เลย
ติดตั้ง Auth.js v5 และ Dependencies
npm install next-auth@beta @auth/prisma-adapter @prisma/client prisma bcryptjs zod
npm install -D @types/bcryptjs
อธิบายแต่ละแพ็กเกจสั้นๆ:
next-auth@beta— ตัว Auth.js v5 เวอร์ชันล่าสุด@auth/prisma-adapter— เชื่อม Auth.js กับ Prisma ORM เพื่อจัดเก็บ Session/User ลงฐานข้อมูลbcryptjs— สำหรับแฮชรหัสผ่านzod— Validate ข้อมูลจากฟอร์ม
สร้าง AUTH_SECRET
npx auth secret
คำสั่งนี้จะสร้างค่า AUTH_SECRET แบบสุ่มแล้วเพิ่มลงในไฟล์ .env.local ให้อัตโนมัติเลย ค่านี้ใช้เข้ารหัส Token และ Session — ห้าม Commit ลง Git เด็ดขาดนะ
ตั้งค่า Environment Variables
เปิดไฟล์ .env.local แล้วเพิ่มค่าเหล่านี้:
# Auth.js
AUTH_SECRET="ค่าที่ generate ไว้"
AUTH_URL="http://localhost:3000"
# Google OAuth
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/myauthdb"
ตั้งค่า Prisma Schema สำหรับ User และ Role
สร้างไฟล์ prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum Role {
USER
EDITOR
ADMIN
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
password String?
role Role @default(USER)
accounts Account[]
sessions Session[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([provider, providerAccountId])
}
model Session {
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VerificationToken {
identifier String
token String
expires DateTime
@@id([identifier, token])
}
จากนั้นรัน:
npx prisma migrate dev --name init
npx prisma generate
จุดสำคัญคือเราเพิ่มฟิลด์ role แบบ Enum ลงใน User Model ตรงนี้แหละที่เป็นหัวใจของระบบ RBAC ที่จะสร้างกันทีหลัง
สร้าง Auth Configuration
แยก Config เป็น 2 ไฟล์ — ทำไมถึงต้องทำแบบนี้?
เรื่องนี้สำคัญมาก ใน Next.js 16 ไฟล์ proxy.ts รันบน Edge Runtime ซึ่งเข้าถึง Node.js APIs ไม่ได้ ดังนั้น Database Adapter อย่าง Prisma ก็ใช้ใน proxy ไม่ได้เช่นกัน วิธีแก้คือแยกเป็น 2 ไฟล์:
auth.config.ts— คอนฟิกพื้นฐานที่ Edge-compatible (สำหรับใช้ใน proxy.ts)auth.ts— คอนฟิกเต็มรูปพร้อม Adapter (สำหรับใช้ในส่วนอื่นทั้งหมด)
ฟังดูยุ่งยาก แต่พอทำจริงแล้วมันค่อนข้าง straightforward เลย
ไฟล์ auth.config.ts (Edge-compatible)
import type { NextAuthConfig } from "next-auth";
export const authConfig = {
pages: {
signIn: "/login",
error: "/login",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");
const isOnAdmin = nextUrl.pathname.startsWith("/admin");
if (isOnAdmin) {
if (!isLoggedIn) return false;
const role = (auth?.user as any)?.role;
if (role !== "ADMIN") {
return Response.redirect(new URL("/dashboard", nextUrl));
}
return true;
}
if (isOnDashboard) {
return isLoggedIn; // redirect ไปหน้า login ถ้ายังไม่ login
}
return true; // หน้าสาธารณะเข้าถึงได้ทุกคน
},
jwt({ token, user }) {
if (user) {
token.role = (user as any).role;
token.id = user.id;
}
return token;
},
session({ session, token }) {
if (session.user) {
session.user.role = token.role as string;
session.user.id = token.id as string;
}
return session;
},
},
providers: [], // เพิ่ม providers ใน auth.ts
} satisfies NextAuthConfig;
อันนี้ต้องระวังนิดนึง — callbacks ทั้ง jwt และ session ต้องอยู่ในไฟล์นี้ ไม่ใช่ auth.ts นะ เพราะ proxy.ts ต้องเข้าถึง role จาก session เพื่อทำ RBAC redirect ได้ ถ้าเอาไปไว้ผิดไฟล์จะ debug กันมึนเลย (เชื่อเถอะ เคยเจอมาแล้ว)
ไฟล์ auth.ts (Full config พร้อม Adapter)
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import Credentials from "next-auth/providers/credentials";
import Google from "next-auth/providers/google";
import bcrypt from "bcryptjs";
import { z } from "zod";
import { authConfig } from "./auth.config";
const loginSchema = z.object({
email: z.string().email("อีเมลไม่ถูกต้อง"),
password: z.string().min(6, "รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร"),
});
export const { handlers, auth, signIn, signOut } = NextAuth({
...authConfig,
adapter: PrismaAdapter(prisma),
session: { strategy: "jwt" },
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
},
},
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "อีเมล", type: "email" },
password: { label: "รหัสผ่าน", type: "password" },
},
async authorize(credentials) {
const parsed = loginSchema.safeParse(credentials);
if (!parsed.success) return null;
const user = await prisma.user.findUnique({
where: { email: parsed.data.email },
});
if (!user || !user.password) return null;
const isValid = await bcrypt.compare(
parsed.data.password,
user.password
);
if (!isValid) return null;
return {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
};
},
}),
],
});
สร้าง Prisma Client Instance
สร้างไฟล์ 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;
ตรงนี้เป็น pattern ที่เห็นกันบ่อยมาก — เราเก็บ Prisma instance ไว้ใน globalThis เพื่อป้องกันไม่ให้ Hot Reload ตอน dev สร้าง connection ใหม่ซ้ำๆ จนฐานข้อมูลล่ม
ตั้งค่า Route Handler สำหรับ Auth.js
สร้างไฟล์ src/app/api/auth/[...nextauth]/route.ts:
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
สั้นมากใช่ไหม? แค่นี้เลย Route Handler ตัวนี้จะจัดการทุก Request ที่เกี่ยวกับ Authentication ให้หมด ไม่ว่าจะ login, logout หรือ callback จาก OAuth Provider
ปกป้อง Route ด้วย proxy.ts
มาถึงส่วนที่น่าสนใจที่สุดแล้ว นี่คือสิ่งที่เปลี่ยนไปจากเวอร์ชันก่อนๆ ของ Next.js ใน Next.js 16 ให้สร้างไฟล์ proxy.ts ที่ root ของโปรเจกต์ (ไม่ใช่ middleware.ts แล้วนะ):
import NextAuth from "next-auth";
import { authConfig } from "./auth.config";
export default NextAuth(authConfig).auth;
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|.*\\.png$).*)",
],
};
proxy.ts ทำหน้าที่หลักๆ คือ:
- ตรวจว่า User มี Session Cookie หรือเปล่า (เป็น Optimistic Check)
- Redirect ไปหน้า Login ถ้าพยายามเข้าหน้าที่ต้องล็อกอิน
- Redirect ไปหน้า Dashboard ถ้า User ไม่มีสิทธิ์เข้าหน้า Admin
ข้อควรจำสำคัญ: proxy.ts ไม่ใช่ Security Boundary ที่แท้จริง มันแค่ทำ Optimistic Check เท่านั้น คุณยังต้อง Validate Session อีกครั้งใน Server Components และ Server Actions เสมอ อย่าพึ่งพามันอย่างเดียว
ขยาย TypeScript Types สำหรับ Role
สร้างไฟล์ src/types/next-auth.d.ts เพื่อให้ TypeScript รู้จักฟิลด์ role:
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 {
role: string;
id: string;
}
}
หลังจาก extend types แล้ว คุณจะเข้าถึง session.user.role ได้แบบ Type-safe ทั้งใน Server Components และ Client Components ไม่ต้อง cast type แบบ as any อีกต่อไป (ยกเว้นใน auth.config.ts ที่ยังจำเป็นต้องใช้)
สร้างระบบ Google OAuth
ขั้นตอนการสร้าง Google OAuth Credentials
- เข้า Google Cloud Console → APIs & Services → Credentials
- คลิก + CREATE CREDENTIALS → เลือก OAuth client ID
- เลือก Application Type เป็น Web Application
- เพิ่ม Authorized JavaScript Origins:
http://localhost:3000 - เพิ่ม Authorized Redirect URIs:
http://localhost:3000/api/auth/callback/google - คัดลอก Client ID และ Client Secret ไปใส่ในไฟล์
.env.local
อย่าลืมว่าเมื่อ Deploy ขึ้น Production ต้องกลับมาเพิ่ม Authorized Origins และ Redirect URIs สำหรับ Domain จริงด้วยนะ ตรงนี้หลายคนลืมแล้วนั่ง debug กันทั้งวัน
ปุ่ม Google Sign-In
สร้างไฟล์ src/components/google-sign-in.tsx:
"use client";
import { signIn } from "next-auth/react";
export function GoogleSignIn() {
return (
<button
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
className="flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2 hover:bg-gray-50"
>
<svg className="h-5 w-5" viewBox="0 0 24 24">
<path
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 01-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
fill="#4285F4"
/>
<path
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
fill="#34A853"
/>
<path
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
fill="#FBBC05"
/>
<path
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
fill="#EA4335"
/>
</svg>
เข้าสู่ระบบด้วย Google
</button>
);
}
สร้างระบบ Credentials (อีเมล/รหัสผ่าน)
Server Action สำหรับสมัครสมาชิก
สร้างไฟล์ src/app/actions/auth.ts:
"use server";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";
import { z } from "zod";
import { signIn } from "@/auth";
const registerSchema = z.object({
name: z.string().min(2, "ชื่อต้องมีอย่างน้อย 2 ตัวอักษร"),
email: z.string().email("อีเมลไม่ถูกต้อง"),
password: z.string().min(6, "รหัสผ่านต้องมีอย่างน้อย 6 ตัวอักษร"),
});
export async function registerUser(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.flatten().fieldErrors };
}
const existing = await prisma.user.findUnique({
where: { email: parsed.data.email },
});
if (existing) {
return { error: { email: ["อีเมลนี้ถูกใช้แล้ว"] } };
}
const hashedPassword = await bcrypt.hash(parsed.data.password, 12);
await prisma.user.create({
data: {
name: parsed.data.name,
email: parsed.data.email,
password: hashedPassword,
role: "USER",
},
});
return { success: true };
}
export async function loginWithCredentials(formData: FormData) {
try {
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirectTo: "/dashboard",
});
} catch (error) {
if ((error as any)?.type === "CredentialsSignin") {
return { error: "อีเมลหรือรหัสผ่านไม่ถูกต้อง" };
}
throw error;
}
}
หน้า Login พร้อมทั้ง Credentials และ Google OAuth
มาทำหน้า Login กัน ส่วนนี้ผมใช้ useActionState hook ตัวใหม่ของ React 19 ซึ่งเหมาะกับ Server Actions มาก
สร้างไฟล์ src/app/login/page.tsx:
"use client";
import { useActionState } from "react";
import { loginWithCredentials } from "@/app/actions/auth";
import { GoogleSignIn } from "@/components/google-sign-in";
export default function LoginPage() {
const [state, formAction, isPending] = useActionState(
loginWithCredentials,
null
);
return (
<div className="mx-auto max-w-md space-y-6 p-8">
<h1 className="text-2xl font-bold">เข้าสู่ระบบ</h1>
<GoogleSignIn />
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">หรือ</span>
</div>
</div>
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
อีเมล
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 w-full rounded-lg border px-3 py-2"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
รหัสผ่าน
</label>
<input
id="password"
name="password"
type="password"
required
className="mt-1 w-full rounded-lg border px-3 py-2"
/>
</div>
{state?.error && (
<p className="text-sm text-red-500">{state.error}</p>
)}
<button
type="submit"
disabled={isPending}
className="w-full rounded-lg bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
>
{isPending ? "กำลังเข้าสู่ระบบ..." : "เข้าสู่ระบบ"}
</button>
</form>
</div>
);
}
จัดการ Session ใน Server Components
นี่คือจุดที่ Auth.js v5 เจ๋งมากจริงๆ ฟังก์ชัน auth() ใช้ได้ตรงๆ ใน Server Components เลย ไม่ต้อง Prop Drilling ไม่ต้อง Context Provider ไม่ต้องอะไรยุ่งยาก:
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return (
<div className="p-8">
<h1 className="text-2xl font-bold">
สวัสดี, {session.user.name}!
</h1>
<p className="text-gray-600">
Role: {session.user.role}
</p>
<p className="text-gray-600">
อีเมล: {session.user.email}
</p>
</div>
);
}
แล้วทำไมต้อง Validate Session ซ้ำอีกทั้งที่ proxy.ts ตรวจแล้ว?
เพราะ proxy.ts ทำแค่ Optimistic Check จาก Cookie เท่านั้น มันไม่ได้เช็คกับฐานข้อมูลว่า Session ยังใช้ได้อยู่ไหม เช่น ถ้า Admin Revoke session ไปแล้ว proxy.ts จะยังมองเห็น Cookie อยู่ การตรวจซ้ำใน Server Components คือ Security Layer ตัวจริง
ใช้ Session ใน Server Actions
"use server";
import { auth } from "@/auth";
export async function updateProfile(formData: FormData) {
const session = await auth();
if (!session?.user) {
throw new Error("ไม่ได้รับอนุญาต");
}
// ดำเนินการอัปเดตโปรไฟล์
// ...
}
ใช้ Session ใน Client Components
สำหรับ Client Components ให้ใช้ SessionProvider ครอบ Layout แล้วเข้าถึง Session ผ่าน useSession hook:
// src/app/layout.tsx
import { SessionProvider } from "next-auth/react";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="th">
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
);
}
// src/components/user-menu.tsx
"use client";
import { useSession, signOut } from "next-auth/react";
export function UserMenu() {
const { data: session } = useSession();
if (!session) return null;
return (
<div className="flex items-center gap-4">
<span>{session.user?.name}</span>
<button
onClick={() => signOut({ callbackUrl: "/" })}
className="rounded bg-red-500 px-3 py-1 text-white"
>
ออกจากระบบ
</button>
</div>
);
}
สร้างระบบ Role-Based Access Control (RBAC)
สร้าง Data Access Layer (DAL)
ส่วนนี้เป็น Best Practice ที่แนะนำมากๆ สร้าง Data Access Layer เพื่อรวมศูนย์การตรวจสอบสิทธิ์ไว้ที่เดียว ทำให้ง่ายต่อการ audit และลดโอกาสลืมเช็คสิทธิ์:
// src/lib/dal.ts
import "server-only";
import { auth } from "@/auth";
import { cache } from "react";
import { redirect } from "next/navigation";
export const verifySession = cache(async () => {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return {
userId: session.user.id,
role: session.user.role,
name: session.user.name,
email: session.user.email,
};
});
export const requireRole = cache(
async (allowedRoles: string[]) => {
const user = await verifySession();
if (!allowedRoles.includes(user.role)) {
redirect("/dashboard");
}
return user;
}
);
ทำไมถึงดี? ดูเหตุผลกัน:
- ใช้
cache()จาก React ทำให้แม้จะเรียกหลาย Components ใน Request เดียว ก็ Call Session แค่ครั้งเดียว - ใช้
server-onlyป้องกันไม่ให้ Import จาก Client Components โดยไม่ตั้งใจ - รวม Authorization Logic ไว้ที่เดียว ไม่ต้องกระจาย auth check ไปทั่วโปรเจกต์
ใช้งาน RBAC ในหน้า Admin
พอมี DAL แล้ว การใช้งานก็ง่ายมาก แค่เรียก requireRole() ที่ต้นฟังก์ชัน:
// src/app/admin/page.tsx
import { requireRole } from "@/lib/dal";
export default async function AdminPage() {
const user = await requireRole(["ADMIN"]);
return (
<div className="p-8">
<h1 className="text-2xl font-bold">แผงควบคุมแอดมิน</h1>
<p>ยินดีต้อนรับ, {user.name} (Admin)</p>
</div>
);
}
// src/app/admin/posts/page.tsx
import { requireRole } from "@/lib/dal";
export default async function ManagePostsPage() {
const user = await requireRole(["ADMIN", "EDITOR"]);
return (
<div className="p-8">
<h1 className="text-2xl font-bold">จัดการบทความ</h1>
<p>สวัสดี, {user.name} ({user.role})</p>
</div>
);
}
ป้องกัน Server Actions ด้วย RBAC
จุดนี้สำคัญและหลายคนมักมองข้าม — การป้องกัน Route อย่างเดียวไม่พอ ต้องป้องกัน Server Actions ด้วย เพราะ Server Actions สามารถถูกเรียกได้โดยตรงผ่าน HTTP Request แม้จะไม่ได้ผ่าน UI ของเรา:
"use server";
import { requireRole } from "@/lib/dal";
import { prisma } from "@/lib/prisma";
export async function deleteUser(userId: string) {
// เฉพาะ Admin เท่านั้นที่ลบ User ได้
await requireRole(["ADMIN"]);
await prisma.user.delete({
where: { id: userId },
});
return { success: true };
}
export async function publishPost(postId: string) {
// Admin และ Editor สามารถ Publish ได้
await requireRole(["ADMIN", "EDITOR"]);
// ... ดำเนินการ publish
}
สรุป Architecture ของระบบ Authentication
ระบบที่เราสร้างขึ้นมามีชั้นป้องกัน 3 ระดับ ซึ่งเป็นแนวคิด Defense-in-Depth:
- proxy.ts (Edge Layer) — ตรวจสอบ Cookie แบบ Optimistic, Redirect ผู้ใช้ที่ไม่มี Session, เช็ค Role เบื้องต้น
- Server Components (Application Layer) — Validate Session จริงผ่าน
auth(), ตรวจสอบ Role ผ่าน DAL - Server Actions (Data Layer) — ตรวจสอบสิทธิ์ก่อนทุกการเขียน/แก้ไขข้อมูลผ่าน
requireRole()
ทั้ง 3 ชั้นนี้ทำงานร่วมกันเพื่อให้มั่นใจว่าไม่ว่าจะมีการ bypass ชั้นใดชั้นหนึ่ง ยังมีชั้นอื่นคอยดักอยู่ ตรงนี้แหละที่ทำให้ระบบ Auth ของเราแข็งแรงจริงๆ
คำถามที่พบบ่อย (FAQ)
Auth.js v5 กับ NextAuth.js v4 ต่างกันอย่างไร?
Auth.js v5 เป็นการเขียนใหม่ทั้งหมดจาก NextAuth.js v4 ความแตกต่างหลักๆ คือ ฟังก์ชัน auth() ใช้ได้ทุกที่แทน getServerSession() ที่ต้องส่ง authOptions ไปตลอด, รองรับ Edge Runtime เต็มรูปแบบ และออกแบบมาสำหรับ App Router เป็นหลัก ส่วน OAuth 1.0 ถูกยกเลิกไปแล้วนะ และต้องใช้ Next.js 14 ขึ้นไป
ต้องใช้ proxy.ts แทน middleware.ts เสมอไหมใน Next.js 16?
ใช่ครับ ใน Next.js 16 ไฟล์ middleware.ts ถูกเปลี่ยนชื่อเป็น proxy.ts อย่างเป็นทางการ ถ้าสร้างโปรเจกต์ใหม่ด้วย Next.js 16 ให้ใช้ proxy.ts เลย แต่ถ้าเป็นโปรเจกต์เก่าที่ใช้ Next.js 15 หรือต่ำกว่า ก็ยังคงใช้ middleware.ts ตามเดิมได้
ใช้ Credentials Provider อย่างเดียวโดยไม่มี Database Adapter ได้ไหม?
ได้ แต่ไม่แนะนำสำหรับ Production เลย เพราะถ้าไม่มี Adapter จะใช้ได้เฉพาะ JWT Strategy เท่านั้น ไม่สามารถจัดเก็บข้อมูล User ลงฐานข้อมูลผ่าน Auth.js ได้ และที่สำคัญคือจะ Revoke Session แบบ Real-time ไม่ได้ สำหรับ Production แนะนำให้ใช้ Database Adapter เสมอ
ทำไมต้องแยก auth.config.ts กับ auth.ts?
เพราะ proxy.ts รันบน Edge Runtime ที่ไม่รองรับ Node.js APIs อย่าง Prisma หรือ native modules ต่างๆ การแยกไฟล์ทำให้ proxy.ts import เฉพาะ auth.config.ts ที่ไม่มี Database Adapter ส่วน auth.ts ที่มี Prisma Adapter จะถูกใช้ใน Server Components และ Route Handlers ที่รันบน Node.js Runtime เต็มรูปแบบ ง่ายๆ คือแยกเพื่อให้ Edge ใช้ได้นั่นเอง
Google OAuth Callback URL ต้องตั้งค่าอย่างไรเมื่อ Deploy ขึ้น Production?
เมื่อ Deploy ขึ้น Vercel หรือ Production ต้องเข้าไปเพิ่ม URL ใน Google Cloud Console ตั้ง Authorized JavaScript Origins เป็น https://yourdomain.com และ Authorized Redirect URIs เป็น https://yourdomain.com/api/auth/callback/google แล้วก็อย่าลืมตั้งค่า AUTH_URL ใน Environment Variables ของ Vercel ให้ตรงกับ Domain จริงด้วยนะ