Autentikacija u Next.js App Router-u sa Auth.js (NextAuth v5)
Saznaj kako implementirati produkcijsku autentikaciju u Next.js App Router-u koristeći Auth.js (NextAuth v5) — upravljanje sesijama, zaštita ruta putem Proxy-ja, OAuth provajderi, credentials login i arhitekturalne odluke koje čuvaju aplikaciju sigurnom i održivom.
TL;DR — Ključni Uvidi
- Auth.js (NextAuth v5) je de facto biblioteka za autentikaciju za Next.js — rukuje sesijama, OAuth provajderima i credentials loginom sa minimalnom konfiguracijom
- Podaci sesije se čuvaju u potpisanom, enkriptovanom kolačiću — baza nije neophodna za osnovnu auth, ali se database adapter preporučuje za produkciju
- Zaštiti rute na nivou Proxy-ja za najbrži mogući redirect — pre nego što stranica počne da se renderuje
- Uvek verifikuj sesiju u Server Komponentama i Server Actions — Proxy je prva linija odbrane, ne jedina
- Auth.js v5 je uveo novu unifikovanu konfiguraciju koja radi kroz Proxy, Server Komponente i API rute bez duplikacije
Zašto Auth.js a ne Nešto Custom
Autentikacija je jedna od oblasti gde je pravljenje sopstvenog rešenja gotovo uvek pogrešna odluka. Sesije, CSRF zaštita, rotacija tokena, OAuth flow-ovi, bezbedno rukovanje kolačićima — svaki od ovih ima dobro dokumentovane načine na koje može poći po zlu, a Auth.js ih rešava po defaultu.
Auth.js (biblioteka ranije poznata kao NextAuth) je u v5 verziji potpuno prerađena za App Router. Unifikovani konfiguracioni model znači da auth logiku definišeš jednom i koristiš je svuda — u Proxy-ju, Server Komponentama, Server Actions i API rutama — bez duplikacije ili nekonzistentnosti.
Instalacija i Konfiguracija
npm install next-auth@beta
npx auth secretKomanda auth secret generiše AUTH_SECRET environment varijablu — ključ koji se koristi za potpisivanje i enkripciju kolačića sesije. Ovo mora biti postavljeno u produkciji.
// auth.ts — jedini izvor istine za auth konfiguraciju
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import { db } from "@/lib/db";
import { verifyPassword } from "@/lib/crypto";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
// OAuth provajder — setup jednim klikom
GitHub({
clientId: process.env.AUTH_GITHUB_ID,
clientSecret: process.env.AUTH_GITHUB_SECRET,
}),
// Credentials provajder — email + lozinka
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Lozinka", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await db.users.findUnique({
where: { email: credentials.email as string },
});
if (!user) return null;
const isValid = await verifyPassword(
credentials.password as string,
user.passwordHash
);
if (!isValid) return null;
return {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
};
},
}),
],
callbacks: {
// Uključi custom polja u JWT token
jwt({ token, user }) {
if (user) {
token.role = user.role;
token.id = user.id;
}
return token;
},
// Uključi custom polja u sesiju
session({ session, token }) {
session.user.role = token.role as string;
session.user.id = token.id as string;
return session;
},
},
pages: {
signIn: "/prijava",
error: "/prijava",
},
});// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";
export const { GET, POST } = handlers;Ovaj jedan auth.ts fajl se importuje svuda gde je auth potreban. Nema duplikacije.
Proširivanje Tipa Sesije
Po defaultu, session.user sadrži samo name, email i image. Za B2B aplikacije gde trebaš role, companyId ili druga custom polja, proširi tipove:
// types/next-auth.d.ts
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: "admin" | "manager" | "viewer";
companyId: string;
} & DefaultSession["user"];
}
}Sa ovim na mestu, session.user.role je potpuno tipiziran kroz celu aplikaciju.
Zaštita Ruta u Proxy-ju
Najbrže mesto za preusmeravanje neautentifikovanih korisnika je proxy.ts — pre nego što bilo koja stranica počne da se renderuje. Auth.js v5 se direktno integriše sa Proxy-jem:
// proxy.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import createIntlMiddleware from "next-intl/middleware";
const intlProxy = createIntlMiddleware({
locales: ["en", "sr"],
defaultLocale: "en",
localePrefix: "always",
});
const ZASTITENE_RUTE = ["/dashboard", "/account", "/admin", "/rfq"];
export async function proxy(request: NextRequest) {
const { pathname } = request.nextUrl;
// Proveri auth za zaštićene rute
const jeZasticena = ZASTITENE_RUTE.some((ruta) =>
pathname.includes(ruta)
);
if (jeZasticena) {
const session = await auth();
if (!session) {
const loginUrl = new URL("/en/prijava", request.url);
loginUrl.searchParams.set("callbackUrl", pathname);
return NextResponse.redirect(loginUrl, { status: 302 });
}
// Kontrola pristupa zasnovana na roli
if (pathname.includes("/admin") && session.user.role !== "admin") {
return NextResponse.redirect(new URL("/en/dashboard", request.url));
}
}
// i18n rutiranje
const response = intlProxy(request);
return response;
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|api/auth).*)"],
};Primeti da matcher isključuje api/auth — Auth.js callback rute moraju biti javno dostupne ili OAuth flow prestaje da radi.
Pristupanje Sesiji u Server Komponentama
Za stranice koje trebaju personalizovati sadržaj ili praviti auth-zavisne upite baze, pristupaj sesiji direktno u Server Komponenti:
// app/[locale]/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
export default async function DashboardPage() {
const session = await auth();
// Dvostruka provera auth-a čak i ako je Proxy već proverio
// Proxy je prva linija odbrane, ne jedina
if (!session) {
redirect("/prijava");
}
// Koristi podatke sesije da skopiš upit baze
const orders = await db.orders.findMany({
where: { companyId: session.user.companyId },
orderBy: { createdAt: "desc" },
take: 10,
});
return (
<div className="mx-auto max-w-5xl px-4 py-12">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<p className="text-sm text-gray-500">
Dobrodošli nazad, {session.user.name}
</p>
</div>
<span className="rounded-full bg-blue-100 px-3 py-1 text-xs font-medium text-blue-700 capitalize">
{session.user.role}
</span>
</div>
<div className="mt-8">
<h2 className="text-lg font-semibold">Nedavne porudžbine</h2>
<ul className="mt-4 divide-y divide-gray-100 rounded-xl border border-gray-200">
{orders.map((order) => (
<li key={order.id} className="flex items-center justify-between px-4 py-3">
<span className="text-sm font-medium">{order.reference}</span>
<span className="text-sm text-gray-500">{order.status}</span>
</li>
))}
</ul>
</div>
</div>
);
}Uvek verifikuj sesiju u Server Komponentama čak i kada Proxy štiti rutu. Odbrana u dubini — ako neko zaobiđe Proxy, stranica i dalje neće prikazati osetljive podatke.
Login i Sign Out UI
// components/LoginForm.tsx
"use client";
import { useActionState } from "react";
import { signIn } from "@/auth";
async function loginAction(
prevState: { error?: string },
formData: FormData
) {
"use server";
try {
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirectTo: "/dashboard",
});
} catch (error) {
return { error: "Neispravan email ili lozinka." };
}
return {};
}
export function LoginForm() {
const [state, action, isPending] = useActionState(loginAction, {});
return (
<form action={action} className="space-y-5">
{state.error && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{state.error}
</div>
)}
<div className="space-y-1.5">
<label className="block text-sm font-medium text-gray-700">Email</label>
<input
name="email"
type="email"
required
autoComplete="email"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="space-y-1.5">
<label className="block text-sm font-medium text-gray-700">Lozinka</label>
<input
name="password"
type="password"
required
autoComplete="current-password"
className="w-full rounded-lg border border-gray-300 px-4 py-2.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<button
type="submit"
disabled={isPending}
className="w-full rounded-lg bg-blue-600 px-6 py-2.5 text-sm font-semibold text-white hover:bg-blue-700 disabled:opacity-60"
>
{isPending ? "Prijavljivanje..." : "Prijavi se"}
</button>
</form>
);
}// components/SignOutButton.tsx
"use client";
import { signOut } from "@/auth";
export function SignOutButton() {
return (
<form
action={async () => {
"use server";
await signOut({ redirectTo: "/" });
}}
>
<button
type="submit"
className="text-sm font-medium text-gray-500 hover:text-gray-700"
>
Odjavi se
</button>
</form>
);
}Database Adapter za Produkciju
Podrazumevana JWT strategija sesije čuva sve u kolačiću. Ovo funkcioniše za jednostavnu auth ali ima ograničenja — ne možeš invalidirati sesije na strani servera, a veličina kolačića ograničava šta možeš da čuvaš.
Za produkcijske B2B aplikacije, koristi database adapter:
npm install @auth/drizzle-adapter
# ili
npm install @auth/prisma-adapter// auth.ts — sa database adapterom
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { db } from "@/lib/db";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(db),
session: { strategy: "database" }, // Čuvaj sesije u DB umesto u kolačiću
providers: [
// ... tvoji provajderi
],
});Sa database adapterom, sesije se čuvaju u tvojoj bazi. Možeš ih programatski opozvati, videti ko je prijavljen i čuvati proizvoljne količine korisničkih podataka bez brige o veličini kolačića.
Zaštita Server Actions
Server Actions koji menjaju osetljive podatke moraju takođe verifikovati sesiju:
// app/[locale]/dashboard/actions.ts
"use server";
import { auth } from "@/auth";
export async function updateOrderStatus(orderId: string, status: string) {
const session = await auth();
// Uvek verifikuj auth u Server Actions
if (!session) {
throw new Error("Neovlašćen pristup");
}
// Autorizacija zasnovana na roli
if (session.user.role !== "admin" && session.user.role !== "manager") {
throw new Error("Nedovoljne dozvole");
}
// Skopira ažuriranje na kompaniju korisnika
await db.orders.update({
where: {
id: orderId,
companyId: session.user.companyId, // Sprečava pristup podacima druge kompanije
},
data: { status },
});
}companyId scope je kritičan u multi-tenant B2B aplikacijama. Čak i ako autentifikovani korisnik nekako pozove akciju sa orderId druge kompanije, companyId provera osigurava da može menjati samo sopstvene podatke.
SEO Razmatranja
Autentikacija i SEO interaguju na važne načine za B2B aplikacije:
Javne vs. privatne stranice: Marketing stranice, listinzi proizvoda i blog sadržaj nikad ne bi trebalo da zahtevaju autentikaciju. Samo dashboard, account i transakcione stranice treba da budu iza auth-a. Prekomerna zaštita stranica ih potpuno uklanja iz Google-ovog indeksa.
Indeksiranje login stranice: Tvoja /prijava stranica treba da ima noindex — nema SEO vrednosti i troši crawl budget.
// app/[locale]/prijava/page.tsx
export const metadata = {
robots: { index: false, follow: false },
};Soft redirecti za botove: Kada Googlebot pogodi zaštićenu rutu, dobija redirect na /prijava. Ovo je očekivano — zaštićene stranice nisu indeksabilne po dizajnu. Osiguraj da je redirect konzistentan.
Česte Greške i Zamke
1. Zaštita ruta samo u Proxy-ju
Proxy se pokreće pre svakog zahteva, ali može biti zaobiđen ili pogrešno konfigurisan. Uvek verifikuj sesiju u Server Komponentama i Server Actions kao drugi sloj. Nikada ne pretpostavljaj da je korisnik autentifikovan samo zato što je stigao do stranice.
2. Izlaganje osetljivih podataka u kolačiću sesije
JWT sesije su enkodirane (nije enkripcija po defaultu u nekim konfiguracijama). Ne čuvaj osetljive podatke — lozinke, podatke o plaćanju, interne ID-jeve koji bi mogli omogućiti enumeraciju — u sesiji. Čuvaj samo ono što je potrebno za UI: ID korisnika, ime, email, rolu.
3. Nescopiranje upita baze na autentifikovanog korisnika
U multi-tenant aplikacijama, svaki upit baze na zaštićenoj stranici mora uključivati companyId ili userId filter. Bez njega, autentifikovani korisnik iz Kompanije A potencijalno može pristupiti podacima Kompanije B manipulacijom parametara zahteva.
4. Zaboravljanje isključivanja Auth.js ruta iz Proxy matcher-a
api/auth rute rukuju OAuth callback-ovima i moraju biti javno dostupne. Uključivanje u Proxy auth proveru će slomiti OAuth flow — korisnici će biti preusmereni na login usred OAuth callback-a, kreirajući beskonačnu petlju.
Često Postavljana Pitanja (FAQ)
Koja je razlika između Auth.js v4 (NextAuth) i v5?
Auth.js v5 je potpuno prerađen specifično za React Server Komponente i App Router. Glavne promene: unifikovana auth() funkcija koja radi u Proxy-ju, Server Komponentama i Server Actions bez posebnih importa; nova konfiguraciona struktura u root auth.ts fajlu; poboljšani TypeScript tipovi. Ako pokrećeš novi projekat, koristi v5.
Treba li koristiti JWT ili database sesije?
Za jednostavne aplikacije ili one bez baze podataka, JWT sesije (čuvane u kolačićima) su fine. Za produkcijske B2B aplikacije, database sesije su snažno preferirane — možeš ih invalidirati na zahtev (prisilna odjava svih uređaja), skaliraju na veće payloade sesije i imaš kompletan revizijski trag aktivnih sesija.
Mogu li koristiti Auth.js sa custom bazom (poput MySQL sa mysql2)?
Da. Auth.js ima zvanične adaptere za Prisma, Drizzle i nekoliko drugih. Za mysql2 bez ORM-a, trebalo bi implementirati custom adapter — što je moguće ali uključuje pisanje implementacija za createUser, getUser, createSession i druge metode. Za većinu projekata, korišćenje Drizzle-a sa MySQL-om je pragmatičan izbor.
Kako da rukovam auth u API rutama koje moraju biti javne?
API rute koje koriste webhook-ovi ili integracije trećih strana treba da koriste auth zasnovan na tajni (deljeni tajni ključ u Authorization headeru) umesto auth-a zasnovanog na sesiji. Kolačići sesije su samo za tokove bazirane na browseru. Za webhook prijemnike, validuj webhook potpis od provajdera umesto toga.
Serijal: Next.js & Modern Web
- 1
- 2
- 3
- 4
- 5
- 6Autentikacija u Next.js App Router-u sa Auth.js (NextAuth v5) (You are here)