Membangun autentikasi fullstack dengan Auth.js: memahami perbedaan session vs JWT, melindungi route dengan middleware, mengimplementasikan login/register email-password, dan integrasi OAuth Google/GitHub.

Mulai episode ini, aplikasi catatan kita mendapatkan lapisan yang membuatnya "bernilai": siapa yang boleh mengakses apa. Autentikasi adalah topik yang paling sering salah diimplementasikan oleh developer — dan kesalahannya berdampak serius. Episode ini membahas auth fullstack dengan Auth.js (NextAuth): session vs JWT, middleware, login/register email-password, dan OAuth.
Mengapa tidak menulis auth sendiri? Karena auth adalah sistem keamanan yang sudah diuji jutaan kali — hashing password, session rotation, dan proteksi timing attack adalah hal yang terlalu mudah salah saat ditulis manual. Gunakan pustaka matang, dan fokuskan energi kalian pada logika produk.
Auth adalah masalah state: bagaimana server mengingat "siapa ini" di setiap request berikutnya? Dua jawaban klasik:
| Aspek | Session (server-side) | JWT (stateless) |
|---|---|---|
| Tempat data | Database / Redis | Token yang ditandatangani |
| Revoke | Langsung (hapus session) | Sulit (harus menunggu expire) |
| Keamanan | Token acak, simpel | Rentan jika secret bocor |
| Cocok untuk | Web app monolith | API stateless, mobile, microservices |
Tip
Untuk aplikasi web penuh yang kita bangun di series ini, session adalah pilihan yang lebih aman dan sederhana. JWT punya tempatnya (mobile API, stateless service), tetapi kekuatan utamanya — tidak perlu penyimpanan server — justru menjadi kelemahan saat kalian butuh revoke session. Pahami keduanya, pilih sesuai kebutuhan.
Install dan atur konfigurasi dasar:
pnpm add next-auth@beta
pnpm add @auth/prisma-adapterimport NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import { comparePassword } from "@/lib/password"
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "database" },
providers: [
GitHub,
Credentials({
async authorize(credentials) {
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
})
if (user && (await comparePassword(credentials.password as string, user.passwordHash))) {
return { id: user.id, email: user.email, name: user.name }
}
return null
},
}),
],
})Model User di Prisma kini butuh kolom passwordHash untuk credential login dan tabel Session/Account untuk session + OAuth — Prisma Adapter mengelola tabel tersebut otomatis. Jangan lupa menjalankan npx prisma migrate dev setelah skema diperbarui.
Server actions dan route handler memanggil auth() langsung. Untuk memblokir akses di level routing, pakai middleware:
import { auth } from "@/auth"
export default auth((req) => {
const isLoggedIn = !!req.auth
const isProtected = req.nextUrl.pathname.startsWith("/notes")
if (isProtected && !isLoggedIn) {
return Response.redirect(new URL("/login", req.nextUrl.origin))
}
})
export const config = {
matcher: ["/notes/:path*", "/api/:path*"],
}Middleware berjalan sebelum route diproses — memblokir akses di gerbang, bukan setelah halaman dirender. Perhatikan bahwa middleware tidak boleh memuat kode besar (Prisma, misalnya) karena berjalan di edge runtime — Auth.js menangani ini secara otomatis.
import { prisma } from "@/lib/prisma"
import { hashPassword } from "@/lib/password"
import { redirect } from "next/navigation"
async function register(formData: FormData) {
"use server"
const email = String(formData.get("email") ?? "")
const password = String(formData.get("password") ?? "")
const passwordHash = await hashPassword(password)
await prisma.user.create({
data: { email, name: email.split("@")[0], passwordHash },
})
redirect("/login")
}
export default function RegisterPage() {
return (
<form action={register}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Daftar</button>
</form>
)
}hashPassword wajib memakai bcrypt/argon2 — bukan MD5/SHA — dan server action harus menangani email duplikat (error constraint P2002 di Prisma) dengan pesan yang ramah.
import { signIn } from "@/auth"
async function login(formData: FormData) {
"use server"
await signIn("credentials", formData)
}
export default function LoginPage() {
return (
<>
<form action={login}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button type="submit">Masuk</button>
</form>
<a href="/api/auth/signin">Masuk dengan GitHub</a>
</>
)
}OAuth (via provider GitHub/Google di konfigurasi Auth.js) menghapus kebutuhan password dari sisi kalian — sekaligus mengurangi tanggung jawab keamanan. Alurnya:
Untuk mengaktifkan, daftarkan aplikasi di GitHub Developer Settings, isi .env dengan AUTH_GITHUB_ID dan AUTH_GITHUB_SECRET, lalu pastikan model Account (dari Prisma Adapter) tersedia. Session OAuth tetap dikelola server-side seperti credential.
Inti yang harus dibawa pulang:
Di episode 7 selanjutnya kita membahas form & validation — server-side validation, error handling, dan optimistic updates — untuk membangun form kompleks yang nyaman dipakai dan aman dari input jahat. Sampai jumpa di episode 7!