Authentication adalah fondasi penting pada hampir semua aplikasi web modern. Ketika aplikasi memiliki halaman seperti dashboard, profil pengguna, pengaturan akun, atau data internal perusahaan, sistem harus dapat membedakan pengguna yang sudah login dan yang belum. Di Next.js 16 dengan App Router, salah satu pendekatan yang sederhana namun efektif adalah menggunakan cookies untuk menyimpan session dan Middleware untuk memproteksi route sebelum halaman dirender.

Pada artikel ini, kita akan membahas konsep dasarnya, alasan teknis di balik pendekatan ini, struktur folder yang rapi, implementasi login sederhana, penyimpanan session menggunakan cookies, serta middleware untuk melindungi halaman dashboard. Contoh yang digunakan sengaja dibuat cukup praktis agar mudah diadaptasi ke aplikasi nyata.

Memahami Authentication pada Aplikasi Web Modern

Secara umum, authentication menjawab pertanyaan: siapa pengguna ini? Setelah pengguna berhasil membuktikan identitasnya, aplikasi biasanya membuat session agar pengguna tidak perlu login ulang pada setiap request.

Dalam aplikasi web modern, ada beberapa pendekatan umum:

  • Session berbasis server: server menyimpan data session, browser hanya menyimpan session identifier.
  • JWT di cookie atau header: informasi identitas atau token akses dibawa oleh klien pada setiap request.
  • OAuth/OpenID Connect: digunakan ketika login melalui Google, GitHub, Microsoft, dan provider lain.

Untuk kasus sederhana hingga menengah, pola yang sangat umum adalah:

  1. Pengguna mengirim email dan password ke endpoint login.
  2. Server memverifikasi kredensial.
  3. Jika valid, server membuat session/token.
  4. Session disimpan dalam cookie yang aman.
  5. Request berikutnya membawa cookie tersebut secara otomatis.
  6. Middleware atau server-side logic mengecek cookie sebelum mengizinkan akses ke halaman tertentu.

Pendekatan ini bekerja baik karena cookie adalah mekanisme bawaan browser yang terintegrasi dengan HTTP. Jika dikonfigurasi dengan benar, cookie juga lebih aman dibanding menyimpan token di localStorage untuk banyak skenario, terutama karena cookie dapat diberi atribut HttpOnly sehingga tidak bisa diakses JavaScript di browser.

Peran Middleware di Next.js

Middleware di Next.js berjalan sebelum request mencapai route atau halaman yang dituju. Ini membuatnya cocok untuk kebutuhan seperti:

  • proteksi halaman privat,
  • redirect pengguna yang belum login,
  • menambahkan logika berbasis path,
  • membaca cookie atau header lebih awal.

Dalam konteks authentication, middleware memungkinkan kita menolak akses ke route seperti /dashboard jika cookie session tidak ada atau tidak valid. Keuntungannya adalah proteksi terjadi lebih awal, sehingga pengguna tidak sempat melihat halaman target terlebih dahulu.

Middleware cocok untuk pemeriksaan ringan dan keputusan routing. Untuk validasi yang lebih berat, seperti query database yang kompleks atau pengecekan izin yang detail, pertimbangkan tetap melakukan verifikasi tambahan di server component, route handler, atau backend API.

Struktur Folder Next.js 16 dengan App Router

Berikut contoh struktur folder yang sederhana untuk implementasi login dan dashboard terproteksi:

my-next-app/
├─ app/
│  ├─ login/
│  │  └─ page.tsx
│  ├─ dashboard/
│  │  └─ page.tsx
│  └─ api/
│     └─ auth/
│        ├─ login/
│        │  └─ route.ts
│        └─ logout/
│           └─ route.ts
├─ lib/
│  └─ auth.ts
└─ middleware.ts

Penjelasan singkat:

  • app/login/page.tsx: halaman form login.
  • app/dashboard/page.tsx: halaman yang hanya boleh diakses pengguna yang sudah login.
  • app/api/auth/login/route.ts: endpoint login untuk memverifikasi kredensial dan mengatur cookie session.
  • app/api/auth/logout/route.ts: endpoint logout untuk menghapus cookie.
  • lib/auth.ts: utilitas untuk membuat dan memverifikasi session.
  • middleware.ts: proteksi route sebelum request diproses lebih lanjut.

Membuat Session Sederhana dengan Cookies

Untuk contoh ini, kita akan membuat session sederhana berbasis token yang ditandatangani. Pada aplikasi production, sebaiknya gunakan mekanisme signing atau encryption yang kuat, misalnya dengan library yang memang dirancang untuk token/session management.

Utilitas Authentication

Buat file lib/auth.ts:

import { createHmac, timingSafeEqual } from 'crypto'

const SESSION_SECRET = process.env.SESSION_SECRET || 'dev-secret-ganti-di-production'

type SessionPayload = {
  userId: string
  email: string
  exp: number
}

function toBase64Url(input: string) {
  return Buffer.from(input).toString('base64url')
}

function fromBase64Url(input: string) {
  return Buffer.from(input, 'base64url').toString('utf8')
}

function sign(value: string) {
  return createHmac('sha256', SESSION_SECRET).update(value).digest('base64url')
}

export function createSessionToken(payload: SessionPayload) {
  const encodedPayload = toBase64Url(JSON.stringify(payload))
  const signature = sign(encodedPayload)
  return `${encodedPayload}.${signature}`
}

export function verifySessionToken(token: string): SessionPayload | null {
  const [encodedPayload, signature] = token.split('.')
  if (!encodedPayload || !signature) return null

  const expectedSignature = sign(encodedPayload)
  const isValid = timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )

  if (!isValid) return null

  const payload = JSON.parse(fromBase64Url(encodedPayload)) as SessionPayload
  if (payload.exp < Date.now()) return null

  return payload
}

Kode di atas melakukan tiga hal:

  • membuat payload session,
  • menandatangani payload dengan secret,
  • memverifikasi bahwa token tidak dimodifikasi dan belum kedaluwarsa.

Ini bukan pengganti penuh sistem auth production-grade, tetapi cukup baik untuk menjelaskan konsep dan alur kerja cookies + middleware.

Membuat Endpoint Login

Buat file app/api/auth/login/route.ts:

import { NextRequest, NextResponse } from 'next/server'
import { createSessionToken } from '@/lib/auth'

export async function POST(request: NextRequest) {
  const body = await request.json()
  const { email, password } = body

  // Contoh sederhana. Di aplikasi nyata, verifikasi ke database
  if (email !== 'admin@example.com' || password !== 'rahasia123') {
    return NextResponse.json(
      { message: 'Email atau password salah' },
      { status: 401 }
    )
  }

  const token = createSessionToken({
    userId: 'user-1',
    email,
    exp: Date.now() + 1000 * 60 * 60 * 24
  })

  const response = NextResponse.json({ message: 'Login berhasil' })

  response.cookies.set('session', token, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 60 * 60 * 24
  })

  return response
}

Beberapa atribut cookie penting:

  • httpOnly: mencegah akses dari JavaScript browser.
  • secure: hanya dikirim lewat HTTPS.
  • sameSite: membantu mengurangi risiko CSRF.
  • maxAge: masa berlaku cookie.
  • path: menentukan cakupan route yang menerima cookie.

Membuat Endpoint Logout

Buat file app/api/auth/logout/route.ts:

import { NextResponse } from 'next/server'

export async function POST() {
  const response = NextResponse.json({ message: 'Logout berhasil' })

  response.cookies.set('session', '', {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    path: '/',
    maxAge: 0
  })

  return response
}

Membuat Halaman Login Sederhana

Buat file app/login/page.tsx:

'use client'

import { FormEvent, useState } from 'react'
import { useRouter } from 'next/navigation'

export default function LoginPage() {
	const router = useRouter()
	const [email, setEmail] = useState('admin@example.com')
	const [password, setPassword] = useState('rahasia123')
	const [error, setError] = useState('')
	const [loading, setLoading] = useState(false)

	async function handleSubmit(e: FormEvent) {
		e.preventDefault()
		setLoading(true)
		setError('')

		const res = await fetch('/api/auth/login', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ email, password })
		})

		const data = await res.json()

		setLoading(false)

		if (!res.ok) {
			setError(data.message || 'Login gagal')
			return
		}

		router.push('/dashboard')
		router.refresh()
	}

	return (
		<div>
			<h1>Login</h1>

			<form onSubmit={handleSubmit}>
				<input
					type="email"
					value={email}
					onChange={(e) => setEmail(e.target.value)}
					placeholder="Email"
				/>

				<input
					type="password"
					value={password}
					onChange={(e) => setPassword(e.target.value)}
					placeholder="Password"
				/>

				<button type="submit" disabled={loading}>
					{loading ? 'Memproses...' : 'Masuk'}
				</button>

				{error && <p>{error}</p>}
			</form>
		</div>
	)
}

Form ini mengirim kredensial ke route handler login. Jika berhasil, server akan mengatur cookie session dan pengguna diarahkan ke /dashboard.

Membuat Middleware untuk Proteksi Dashboard

Sekarang kita buat file middleware.ts di root project:

import { NextRequest, NextResponse } from 'next/server'
import { verifySessionToken } from '@/lib/auth'

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const sessionCookie = request.cookies.get('session')?.value

  const isDashboardRoute = pathname.startsWith('/dashboard')
  const isLoginRoute = pathname.startsWith('/login')

  const session = sessionCookie ? verifySessionToken(sessionCookie) : null

  if (isDashboardRoute && !session) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('redirect', pathname)
    return NextResponse.redirect(loginUrl)
  }

  if (isLoginRoute && session) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/dashboard/:path*', '/login']
}

Middleware ini melakukan dua hal:

  • jika pengguna belum login dan mengakses /dashboard, redirect ke /login,
  • jika pengguna sudah login dan membuka /login, redirect ke /dashboard.

Dengan matcher, middleware hanya berjalan pada route tertentu sehingga lebih efisien dibanding memproses semua request.

Membuat Halaman Dashboard

Buat file app/dashboard/page.tsx:

import { cookies } from 'next/headers'
import { verifySessionToken } from '@/lib/auth'

export default async function DashboardPage() {
	const cookieStore = await cookies()
	const sessionToken = cookieStore.get('session')?.value
	const session = sessionToken ? verifySessionToken(sessionToken) : null

	return (
		<div>
			<h1>Dashboard</h1>

			<p>
				Selamat datang, {session?.email}
			</p>

			<form action="/api/auth/logout" method="POST">
				<button type="submit">
					Logout
				</button>
			</form>
		</div>
	)
}

Walaupun route ini sudah diproteksi oleh middleware, pengecekan ulang di server component tetap berguna sebagai lapisan pertahanan tambahan. Ini penting karena middleware sebaiknya tidak dianggap sebagai satu-satunya proteksi untuk seluruh sistem.

Menjalankan Project

Jika project belum dibuat, Anda bisa memulai dengan perintah berikut:

npx create-next-app@latest next-auth-middleware-demo
cd next-auth-middleware-demo

# Jika sudah ada project, cukup jalankan ini
npm run dev

Tambahkan juga environment variable untuk secret session:

# .env.local
SESSION_SECRET=isi-dengan-random-secret-yang-panjang-dan-aman

Kenapa Pendekatan Ini Bekerja?

Kombinasi cookie + middleware efektif karena masing-masing menangani tanggung jawab yang berbeda:

  • Cookie menyimpan bukti session secara persisten di sisi browser dan dikirim otomatis pada setiap request yang relevan.
  • Route handler bertanggung jawab membuat dan menghapus session.
  • Middleware memutuskan apakah request boleh diteruskan atau harus diarahkan ulang.
  • Server component / backend tetap dapat memverifikasi session sebelum menampilkan data sensitif.

Pemisahan ini membuat arsitektur lebih mudah dipelihara dan diperluas. Misalnya, nanti Anda dapat mengganti verifikasi hardcoded menjadi query database tanpa perlu mengubah alur utama aplikasi.

Kesalahan Umum dan Tips Debugging

1. Cookie tidak tersimpan

Penyebab umum:

  • atribut secure: true digunakan di lingkungan HTTP lokal,
  • response tidak benar-benar mengatur cookie,
  • domain/path cookie tidak sesuai.

Periksa tab Application atau Storage di browser developer tools untuk memastikan cookie benar-benar dibuat.

2. Middleware tidak berjalan

Pastikan:

  • file bernama tepat middleware.ts,
  • berada di root project,
  • matcher mencakup route yang diuji.

3. Redirect berulang

Biasanya terjadi karena logika login route dan protected route saling bertentangan. Pastikan middleware membedakan dengan jelas antara route publik dan privat.

4. Session dianggap invalid

Periksa:

  • SESSION_SECRET konsisten di environment yang sama,
  • format token tidak rusak,
  • waktu kedaluwarsa (exp) masih valid.

Praktik Terbaik untuk Production

Untuk aplikasi production, jangan berhenti pada contoh minimal di atas. Terapkan beberapa praktik berikut:

  • Simpan password dalam bentuk hash menggunakan algoritma seperti Argon2 atau bcrypt, jangan pernah plaintext.
  • Gunakan HTTPS agar cookie dengan atribut secure benar-benar efektif.
  • Selalu gunakan HttpOnly cookie untuk session agar tidak bisa diakses script di browser.
  • Tambahkan proteksi CSRF terutama jika memakai cookie untuk autentikasi pada request state-changing.
  • Rotasi session setelah login dan pada momen sensitif untuk mengurangi risiko session fixation.
  • Gunakan session store atau solusi auth yang matang jika kebutuhan sudah kompleks, misalnya multi-device session, revoke token, audit log, atau role-based access control.
  • Verifikasi authorization di server, bukan hanya authentication. Login saja tidak berarti pengguna boleh mengakses semua resource.
  • Batasi percobaan login dengan rate limiting untuk mencegah brute force.
  • Log aktivitas penting seperti login berhasil, gagal, logout, dan percobaan akses ilegal.
  • Jangan menaruh data sensitif langsung di payload token tanpa enkripsi jika token dapat dibaca klien.

Jika aplikasi Anda berkembang menjadi lebih kompleks, pertimbangkan penggunaan solusi seperti NextAuth/Auth.js, session store berbasis Redis, atau backend auth terpisah. Namun memahami mekanisme dasar cookies dan middleware tetap penting, karena membantu Anda mengambil keputusan arsitektur yang lebih tepat.

Penutup

Authentication di Next.js 16 dengan App Router dapat diimplementasikan secara bersih menggunakan kombinasi route handler, cookies, dan middleware. Cookie berperan sebagai penyimpan session yang dikirim otomatis oleh browser, sementara middleware memproteksi route seperti /dashboard bahkan sebelum halaman dirender.

Pendekatan ini cocok untuk banyak kebutuhan aplikasi internal, dashboard admin, atau produk SaaS sederhana, selama Anda tetap memperhatikan keamanan implementasinya. Untuk production, fokus utama bukan hanya membuat pengguna bisa login, tetapi juga memastikan session aman, validasi dilakukan di server, dan akses terhadap data sensitif benar-benar dibatasi sesuai hak pengguna.