De ce ai nevoie de un sistem de autentificare solid în Next.js 16
Să fim sinceri — autentificarea nu e partea cea mai distractivă a unui proiect. Dar e absolut esențială. Fiecare aplicație de producție, de la un simplu panou de administrare până la o platformă SaaS completă, are nevoie de un mecanism prin care utilizatorii să-și dovedească identitatea. În ecosistemul Next.js 16, Auth.js v5 (fostul NextAuth.js) a devenit soluția oficială recomandată, complet reproiectată pentru App Router, Server Components și noua arhitectură bazată pe proxy.ts.
Am lucrat cu NextAuth încă de la versiunea 3 și pot spune că v5 e un salt uriaș. Designul e mult mai curat.
Acest ghid te va duce pas cu pas prin tot procesul: de la instalare și configurare OAuth, la protecția rutelor cu proxy.ts, gestionarea sesiunilor în Server Components, autorizare bazată pe roluri (RBAC) și integrarea cu Drizzle ORM pentru sesiuni persistente. La final, vei avea o arhitectură de autentificare completă, gata de producție.
Cerințe preliminare
Înainte de a începe, asigură-te că ai la dispoziție:
- Node.js 18.18 sau mai recent instalat
- Un proiect Next.js 16 creat cu App Router
- Cunoștințe de bază despre React Server Components și Server Actions
- Un cont GitHub și/sau Google Cloud Console pentru credențialele OAuth
Dacă nu ai încă un proiect Next.js 16, poți crea unul rapid cu npx create-next-app@latest.
Instalarea și configurarea Auth.js v5
Instalarea pachetelor
Auth.js v5 e publicat sub tag-ul beta al pachetului next-auth. Instalarea e simplă — o singură comandă:
npm install next-auth@beta
Dacă plănuiești să folosești sesiuni în baza de date cu Drizzle ORM (și sincer, pentru proiecte serioase merită), instalează și adaptorul:
npm install drizzle-orm @auth/drizzle-adapter
npm install drizzle-kit --save-dev
Apoi generează secretul de autentificare — practic o cheie criptografică pentru semnarea token-urilor JWT:
npx auth secret
Comanda asta creează automat variabila AUTH_SECRET în fișierul .env.local. Nici nu trebuie să faci nimic manual.
Structura fișierelor
Iată structura pe care o recomand pentru un proiect Next.js 16 cu autentificare Auth.js v5:
proiect/
├── auth.ts # Configurare principală Auth.js
├── auth.config.ts # Configurare edge-safe (pentru proxy)
├── proxy.ts # Înlocuiește middleware.ts
├── .env.local # Variabile de mediu
├── app/
│ ├── api/auth/
│ │ └── [...nextauth]/
│ │ └── route.ts # Route handler Auth.js
│ ├── lib/
│ │ └── dal.ts # Data Access Layer
│ ├── login/
│ │ └── page.tsx # Pagina de autentificare
│ └── dashboard/
│ └── page.tsx # Pagină protejată
├── db/
│ ├── schema.ts # Schema Drizzle ORM
│ └── index.ts # Conexiune bază de date
└── types/
└── next-auth.d.ts # Extensii TypeScript
Variabilele de mediu
Creează fișierul .env.local cu următoarele variabile:
AUTH_SECRET=cheie-generata-automat
# Google OAuth
AUTH_GOOGLE_ID=id-client-google
AUTH_GOOGLE_SECRET=secret-client-google
# GitHub OAuth
AUTH_GITHUB_ID=id-client-github
AUTH_GITHUB_SECRET=secret-client-github
# Baza de date (opțional, pentru Drizzle)
AUTH_DRIZZLE_URL=postgres://user:password@host:5432/dbname
Un detaliu drăguț: Auth.js v5 detectează automat variabilele prefixate cu AUTH_. Adică AUTH_GITHUB_ID și AUTH_GITHUB_SECRET sunt folosite automat ca clientId/clientSecret pentru provider-ul GitHub. Și AUTH_URL (fostul NEXTAUTH_URL) nu mai e obligatoriu în majoritatea mediilor — un lucru în minus de care să-ți faci griji.
Configurarea principală — fișierul auth.ts
Aici vine schimbarea de paradigmă. În Auth.js v5, totul se exportă dintr-un singur apel NextAuth() — funcțiile auth, signIn, signOut și handlers:
// auth.ts (rădăcina proiectului)
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
export const { handlers, signIn, signOut, auth } = NextAuth({
pages: {
signIn: "/login",
error: "/login",
},
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,
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) token.role = user.role
return token
},
async session({ session, token }) {
if (token.sub && session.user) {
session.user.id = token.sub
session.user.role = token.role as string
}
return session
},
},
})
Nu mai există un obiect authOptions separat pe care să-l transmiți manual peste tot. Dacă ai folosit NextAuth v4, știi exact cât de enervant era asta. Designul simplificat din v5 elimină o întreagă clasă de bug-uri frecvente.
Route Handler-ul pentru Auth.js
Creează route handler-ul care gestionează toate rutele de autentificare (login, logout, callback OAuth):
// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"
export const { GET, POST } = handlers
Da, doar două linii de cod. Auth.js se ocupă de tot restul. Serios.
Configurarea provider-ilor OAuth
Google OAuth
Pentru a configura autentificarea cu Google:
- Accesează Google Cloud Console → APIs & Services → Credentials
- Creează un nou OAuth 2.0 Client ID de tip „Web application"
- Adaugă la Authorized redirect URIs:
http://localhost:3000/api/auth/callback/google - Copiază Client ID și Client Secret în
.env.local
GitHub OAuth
Pentru GitHub e chiar mai simplu:
- Accesează GitHub Settings → Developer settings → OAuth Apps
- Creează o nouă aplicație OAuth
- Setează Authorization callback URL:
http://localhost:3000/api/auth/callback/github - Copiază Client ID și Client Secret în
.env.local
Auth.js v5 suportă peste 80 de provideri OAuth preconfigurati — de la Apple și Microsoft până la Discord și Spotify. Deci indiferent ce provider ai nevoie, sunt șanse mari să fie deja suportat.
Protecția rutelor cu proxy.ts
Asta e una dintre schimbările majore din Next.js 16. Fișierul middleware.ts a fost redenumit în proxy.ts, iar funcția exportată se numește acum proxy în loc de middleware. Partea interesantă? Acum rulează pe runtime-ul Node.js (nu Edge, ca înainte), ceea ce înseamnă acces complet la API-urile Node.js.
Protecție simplă — o singură linie
Cea mai simplă formă de protecție a rutelor arată așa:
// proxy.ts (rădăcina proiectului)
export { auth as proxy } from "@/auth"
O singură linie. Re-exportă funcția auth ca proxy, care verifică automat validitatea sesiunii. Nu poți obține ceva mai simplu de atât.
Protecție cu redirecționare personalizată
Bun, dar în practică vei avea nevoie de un control mai fin. Iată cum arată o configurare cu redirecționări personalizate:
// proxy.ts
import { auth } from "@/auth"
export const proxy = auth((req) => {
const isLoggedIn = !!req.auth
const isOnLogin = req.nextUrl.pathname === "/login"
const isPublicPath = ["/", "/about", "/pricing"].includes(
req.nextUrl.pathname
)
// Redirecționează utilizatorii neautentificați
if (!isLoggedIn && !isOnLogin && !isPublicPath) {
return Response.redirect(
new URL("/login", req.nextUrl.origin)
)
}
// Redirecționează utilizatorii autentificați departe de login
if (isLoggedIn && isOnLogin) {
return Response.redirect(
new URL("/dashboard", req.nextUrl.origin)
)
}
})
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)"
],
}
Configurare edge-safe (pentru adaptoare de bază de date)
Dacă folosești un adaptor de bază de date (cum ar fi Drizzle sau Prisma), proxy.ts nu poate importa direct conexiunea la baza de date. Soluția e să separi configurarea în două fișiere — un pattern pe care o să-l întâlnești frecvent:
// auth.config.ts — configurare edge-safe (fără adaptor DB)
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import type { NextAuthConfig } from "next-auth"
export default {
providers: [Google, GitHub],
pages: {
signIn: "/login",
},
} satisfies NextAuthConfig
// proxy.ts — folosește configurarea edge-safe
import authConfig from "./auth.config"
import NextAuth from "next-auth"
const { auth } = NextAuth(authConfig)
export const proxy = auth(async function proxy(req) {
if (!req.auth && req.nextUrl.pathname !== "/login") {
return Response.redirect(
new URL("/login", req.nextUrl.origin)
)
}
})
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico).*)"
],
}
Gestionarea sesiunilor în Server Components
Probabil cea mai mare îmbunătățire din Auth.js v5 este funcția universală auth(). Ea înlocuiește complet getServerSession, getToken și useSession din versiunea anterioară. O singură funcție care funcționează peste tot — Server Components, Route Handlers, Server Actions și proxy.ts.
Sincer, asta mi-a simplificat codul enorm.
Verificarea sesiunii într-un Server Component
// app/dashboard/page.tsx
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>
<h1>Bine ai venit, {session.user.name}</h1>
<p>Email: {session.user.email}</p>
<p>Rol: {session.user.role}</p>
</div>
)
}
Login și Logout cu Server Actions
Auth.js v5 expune funcțiile signIn și signOut care pot fi apelate direct din Server Actions. Nici un API route separat, nici un fetch manual:
// app/login/page.tsx
import { signIn } from "@/auth"
export default function LoginPage() {
return (
<div>
<h1>Autentificare</h1>
<form action={async () => {
"use server"
await signIn("google")
}}>
<button type="submit">
Conectare cu Google
</button>
</form>
<form action={async () => {
"use server"
await signIn("github")
}}>
<button type="submit">
Conectare cu GitHub
</button>
</form>
</div>
)
}
// components/LogoutButton.tsx
import { signOut } from "@/auth"
export default function LogoutButton() {
return (
<form action={async () => {
"use server"
await signOut()
}}>
<button type="submit">Deconectare</button>
</form>
)
}
Accesul la sesiune din Client Components
Pentru componentele interactive care au nevoie de datele sesiunii pe client, folosești SessionProvider și hook-ul useSession. E destul de standard:
// app/layout.tsx
import { SessionProvider } from "next-auth/react"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="ro">
<body>
<SessionProvider>
{children}
</SessionProvider>
</body>
</html>
)
}
// components/UserInfo.tsx
"use client"
import { useSession } from "next-auth/react"
export default function UserInfo() {
const { data: session, status } = useSession()
if (status === "loading") return <p>Se încarcă...</p>
if (!session) return <p>Nu ești autentificat.</p>
return (
<div>
<p>Salut, {session.user?.name}</p>
<img
src={session.user?.image ?? ""}
alt="Avatar"
width={40}
height={40}
/>
</div>
)
}
JWT vs. sesiuni în baza de date
Auth.js v5 oferă două strategii de gestionare a sesiunilor, și alegerea contează destul de mult:
- JWT (
session: { strategy: "jwt" }) — Token-ul e stocat într-un cookie criptat. Ideal pentru medii serverless și edge. Nu necesită interogări la baza de date pentru fiecare cerere. E strategia implicită și probabil cea potrivită pentru majoritatea proiectelor. - Baza de date (
session: { strategy: "database" }) — Sesiunea e stocată în baza de date. Necesită tabelulsessions. Alege asta când ai nevoie de revocarea sesiunilor pe server (de exemplu, un buton „deconectează-te de pe toate dispozitivele").
Integrarea cu Drizzle ORM pentru sesiuni persistente
Dacă vrei să stochezi datele utilizatorilor și sesiunile în baza de date (și pentru orice proiect serios, probabil vrei), iată cum configurezi Drizzle ORM cu Auth.js v5 și PostgreSQL.
Schema bazei de date
Schema e un pic lungă, dar fiecare tabel are rolul său clar definit:
// db/schema.ts
import {
pgTable,
text,
primaryKey,
integer,
timestamp,
boolean,
} from "drizzle-orm/pg-core"
import type { AdapterAccountType } from "@auth/core/adapters"
export const users = pgTable("user", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").unique(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
image: text("image"),
role: text("role").default("user"),
})
export const accounts = pgTable(
"account",
{
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type")
.$type<AdapterAccountType>()
.notNull(),
provider: text("provider").notNull(),
providerAccountId: text("providerAccountId").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
},
(account) => ({
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId],
}),
})
)
export const sessions = pgTable("session", {
sessionToken: text("sessionToken").primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
})
export const verificationTokens = pgTable(
"verificationToken",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(vt) => ({
compositePk: primaryKey({
columns: [vt.identifier, vt.token],
}),
})
)
Conexiunea la baza de date
// db/index.ts
import { neon } from "@neondatabase/serverless"
import { drizzle } from "drizzle-orm/neon-http"
const sql = neon(process.env.AUTH_DRIZZLE_URL!)
export const db = drizzle(sql)
Configurarea adaptorului în auth.ts
Acum leagă totul împreună în fișierul principal auth.ts:
// auth.ts — versiune completă cu Drizzle ORM
import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import GitHub from "next-auth/providers/github"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { db } from "@/db"
import {
users,
accounts,
sessions,
verificationTokens,
} from "@/db/schema"
import authConfig from "./auth.config"
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,
sessionsTable: sessions,
verificationTokensTable: verificationTokens,
}),
session: { strategy: "jwt" },
...authConfig,
callbacks: {
async jwt({ token, user }) {
if (user) token.role = user.role
return token
},
async session({ session, token }) {
if (token.sub && session.user) {
session.user.id = token.sub
session.user.role = token.role as string
}
return session
},
},
})
Apoi aplică migrările și ești gata:
npx drizzle-kit generate
npx drizzle-kit push
Autorizare bazată pe roluri (RBAC)
Autentificarea confirmă cine ești. Autorizarea decide ce poți face. Sunt două lucruri diferite, și e important să nu le confunzi.
Implementarea RBAC (Role-Based Access Control) în Auth.js v5 se bazează pe callback-urile jwt și session pe care le-am configurat mai sus.
Extinderea tipurilor TypeScript
Mai întâi, extinde tipurile pentru a include câmpul role. Fără asta, TypeScript o să se plângă (pe bună dreptate):
// types/next-auth.d.ts
import { DefaultSession } from "next-auth"
declare module "next-auth" {
interface User {
role?: string
}
interface Session {
user: {
id: string
role: string
} & DefaultSession["user"]
}
}
declare module "next-auth/jwt" {
interface JWT {
role?: string
}
}
Pattern-ul Data Access Layer (DAL)
Echipa Next.js recomandă un Data Access Layer — un modul centralizat care verifică autorizarea înainte de fiecare acces la date. E un pattern excelent pe care l-am adoptat și eu în proiectele mele:
// app/lib/dal.ts
import "server-only"
import { cache } from "react"
import { auth } from "@/auth"
import { redirect } from "next/navigation"
export const verifySession = cache(async () => {
const session = await auth()
if (!session?.user) {
redirect("/login")
}
return session
})
export const requireAdmin = cache(async () => {
const session = await verifySession()
if (session.user.role !== "admin") {
redirect("/unauthorized")
}
return session
})
Directiva import "server-only" e un detaliu important — garantează că acest modul generează o eroare de build dacă e importat accidental dintr-un Client Component. O plasă de siguranță care te salvează de greșeli subtile.
Utilizarea DAL în Server Components
// app/admin/page.tsx
import { requireAdmin } from "@/app/lib/dal"
export default async function AdminPage() {
const session = await requireAdmin()
return (
<div>
<h1>Panou de Administrare</h1>
<p>Bine ai venit, {session.user.name}!</p>
<p>Rol: {session.user.role}</p>
</div>
)
}
Protejarea Server Actions
Nu uita — Server Actions sunt endpoint-uri publice. Oricine poate trimite un request POST către ele. De aceea e esențial să verifici autorizarea în fiecare action:
// app/actions.ts
"use server"
import { verifySession, requireAdmin } from "@/app/lib/dal"
import { db } from "@/db"
export async function deletePost(id: number) {
const session = await requireAdmin()
if (typeof id !== "number") {
throw new Error("ID invalid")
}
await db.delete(posts).where(eq(posts.id, id))
revalidatePath("/posts")
}
export async function updateProfile(formData: FormData) {
const session = await verifySession()
const name = formData.get("name") as string
await db
.update(users)
.set({ name })
.where(eq(users.id, session.user.id))
}
Modelul de securitate pe trei straturi
Next.js 16 recomandă un model de securitate pe trei straturi. E un concept important, deci merită să-l înțelegi bine:
- proxy.ts (stratul 1) — Verificare optimistă bazată pe existența cookie-ului. E rapid, dar nu este granița ta de securitate. Servește doar pentru redirecționări UX, atât.
- Server Components / Server Actions (stratul 2) — Verifică identitatea și permisiunile la fiecare operație folosind
auth()și DAL. Aceasta e granița reală de securitate. - Baza de date (stratul 3) — Ultima linie de apărare. Prin Row-Level Security (RLS) sau validări la nivel de query, te asiguri că nici un Server Action configurat greșit nu poate scurge date.
Regulă de aur: Nu te baza exclusiv pe proxy.ts pentru autorizare. Verifică întotdeauna sesiunea cât mai aproape de locul unde accesezi datele. Am văzut proiecte care se bazau doar pe middleware pentru securitate — și au avut probleme serioase.
Formularul complet de autentificare cu credențiale
Pe lângă providerii OAuth, poți implementa și autentificarea clasică cu email și parolă. Nu e întotdeauna recomandat (OAuth e în general mai sigur), dar uneori e necesar. Iată un exemplu complet cu validare Zod:
// lib/validation.ts
import { z } from "zod"
export const loginSchema = z.object({
email: z
.string()
.email("Adresa de email nu este validă"),
password: z
.string()
.min(8, "Parola trebuie să aibă cel puțin 8 caractere"),
})
// auth.ts — adaugă provider-ul Credentials
import Credentials from "next-auth/providers/credentials"
import { loginSchema } from "@/lib/validation"
import bcrypt from "bcryptjs"
import { db } from "@/db"
import { eq } from "drizzle-orm"
import { users } from "@/db/schema"
// În lista de provideri:
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Parolă", type: "password" },
},
async authorize(credentials) {
const parsed = loginSchema.safeParse(credentials)
if (!parsed.success) return null
const user = await db.query.users.findFirst({
where: eq(users.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,
}
},
})
Componenta formularului de login
Aici folosim hook-ul useActionState din React 19+ pentru a gestiona starea formularului — e mult mai curat decât soluțiile vechi cu useState și fetch:
// app/login/LoginForm.tsx
"use client"
import { useActionState } from "react"
import { authenticate } from "@/app/login/actions"
export default function LoginForm() {
const [errorMsg, formAction, isPending] = useActionState(
authenticate,
undefined
)
return (
<form action={formAction}>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
required
/>
</div>
<div>
<label htmlFor="password">Parolă</label>
<input
id="password"
name="password"
type="password"
required
/>
</div>
{errorMsg && (
<p style={{ color: "red" }}>{errorMsg}</p>
)}
<button type="submit" disabled={isPending}>
{isPending ? "Se conectează..." : "Conectare"}
</button>
</form>
)
}
// app/login/actions.ts
"use server"
import { signIn } from "@/auth"
import { AuthError } from "next-auth"
export async function authenticate(
prevState: string | undefined,
formData: FormData
) {
try {
await signIn("credentials", formData)
} catch (error) {
if (error instanceof AuthError) {
switch (error.type) {
case "CredentialsSignin":
return "Email sau parolă incorectă."
default:
return "A apărut o eroare. Încearcă din nou."
}
}
throw error
}
}
Migrarea de la NextAuth v4 la Auth.js v5 — rezumat
Dacă ai un proiect existent cu NextAuth v4, iată principalele modificări pe care trebuie să le faci. Am trecut prin procesul ăsta de câteva ori, și chiar nu e atât de complicat pe cât pare:
NextAuthOptions→NextAuthConfiggetServerSession(authOptions)→auth()getToken({ req })→auth()@next-auth/prisma-adapter→@auth/prisma-adapter- Prefixul cookie-urilor:
next-auth→authjs NEXTAUTH_SECRET→AUTH_SECRETNEXTAUTH_URL→AUTH_URL(nu mai e obligatoriu)- Export middleware:
export { default } from "next-auth/middleware"→export { auth as proxy } from "@/auth"
Întrebări frecvente
Cum configurez autentificarea în Next.js 16 cu Auth.js v5?
Instalează pachetul next-auth@beta, generează un secret cu npx auth secret, creează fișierul auth.ts cu configurarea provider-ilor, apoi adaugă route handler-ul în app/api/auth/[...nextauth]/route.ts. Totul se exportă dintr-un singur apel NextAuth() — auth, signIn, signOut și handlers.
Care e diferența între proxy.ts și middleware.ts pentru autentificare?
În Next.js 16, proxy.ts înlocuiește middleware.ts. Diferența principală: proxy.ts rulează pe runtime-ul Node.js (nu Edge), oferind acces complet la API-urile Node.js, conexiuni directe la baza de date și biblioteci de autentificare complete. Funcția exportată se numește proxy în loc de middleware.
Cum protejez rutele în Server Components cu Auth.js v5?
Folosește funcția auth() exportată din auth.ts pentru a verifica sesiunea. Apeleaz-o la începutul oricărui Server Component, Route Handler sau Server Action. Dacă sesiunea lipsește, redirecționează cu redirect("/login"). Pentru un pattern reutilizabil, implementează un Data Access Layer (DAL) cu funcții precum verifySession().
Ce bibliotecă de autentificare să aleg în 2026 pentru Next.js?
Auth.js v5 e soluția oficială recomandată de echipa Next.js, cu suport nativ pentru App Router, Server Components și proxy.ts. Alternative populare includ Clerk (autentificare gestionată), Better Auth și Auth0. Alege Auth.js dacă vrei control total și cod open-source, sau un serviciu gestionat dacă preferi setup minim.
Cum implementez autorizarea bazată pe roluri (RBAC) în Next.js 16?
Adaugă un câmp role în tabelul users din baza de date. Propagă-l în token-ul JWT prin callback-ul jwt și apoi în sesiune prin callback-ul session. Creează un Data Access Layer (DAL) cu funcții precum requireAdmin() care verifică rolul înainte de a permite accesul. Extinde tipurile TypeScript cu declare module "next-auth" pentru type-safety complet.