Autentikasi Supabase di Next.js App Router sebaiknya dibangun dengan dua jenis client: browser client untuk interaksi dari Client Component dan server client untuk Server Component, Server Action, Route Handler, serta middleware. Cookie session perlu diteruskan dengan benar agar halaman server dapat mengenali pengguna dan route terlindungi tidak hanya mengandalkan pemeriksaan di sisi browser.
Panduan ini menggunakan package @supabase/ssr dan TypeScript. Contoh di bawah mencakup autentikasi email-password, OAuth, pengelolaan session, middleware, validasi input, serta konfigurasi redirect yang aman.
1. Prasyarat dan instalasi
Buat project Supabase, lalu siapkan aplikasi Next.js yang menggunakan App Router. Instal package berikut:
npm install @supabase/supabase-js @supabase/ssr
Di dashboard Supabase, aktifkan provider autentikasi yang diperlukan pada menu Authentication. Untuk login email-password, pastikan provider Email aktif. Jika menggunakan OAuth, konfigurasikan provider seperti Google dan isi client ID serta client secret sesuai petunjuk provider tersebut.
2. Konfigurasi environment variable
Buat file .env.local di root project:
NEXT_PUBLIC_SUPABASE_URL=https://project-ref.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-or-anon-key
Nama key publik dapat berupa publishable key pada project Supabase yang lebih baru atau anon key pada konfigurasi lama. Keduanya ditujukan untuk digunakan oleh client aplikasi, sedangkan service role key tidak boleh ditempatkan pada variable dengan prefix NEXT_PUBLIC_ dan tidak boleh dikirim ke browser.
Restart server development setelah mengubah environment variable. Tambahkan .env.local ke .gitignore dan jangan meng-commit secret OAuth maupun service role key.
3. Membuat Supabase client
Browser client
Browser client dipakai oleh Client Component untuk registrasi, login, logout, dan memulai OAuth. Buat file lib/supabase/client.ts:
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
}
Tanda ! hanya memberi tahu TypeScript bahwa variable tersedia. Pada aplikasi produksi, environment variable tetap harus dipastikan ada melalui konfigurasi deployment; tanda tersebut bukan validasi runtime.
Server client
Server client membaca dan menulis cookie session melalui API cookie Next.js. Buat file lib/supabase/server.ts:
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options)
})
} catch {
// Server Component tidak selalu dapat menulis cookie.
// Middleware atau Server Action tetap menangani pembaruan session.
}
},
},
}
)
}
Client ini dapat digunakan pada Server Component, Server Action, dan Route Handler. Jangan memakai browser client untuk membaca session di server karena browser client tidak terhubung langsung dengan cookie request server.
4. Registrasi dan login dengan email-password
Validasi input di server atau sebelum memanggil Supabase. Validasi client berguna untuk pengalaman pengguna, tetapi tidak boleh menjadi satu-satunya lapisan karena dapat dilewati.
Contoh Client Component berikut melakukan registrasi, login, dan logout melalui browser client:
'use client'
import { FormEvent, useState } from 'react'
import { useRouter } from 'next/navigation'
import { createClient } from '@/lib/supabase/client'
export default function AuthForm() {
const supabase = createClient()
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [message, setMessage] = useState('')
function validInput() {
if (!email.includes('@')) {
setMessage('Masukkan email yang valid.')
return false
}
if (password.length < 8) {
setMessage('Password minimal 8 karakter.')
return false
}
return true
}
async function signUp(event: FormEvent) {
event.preventDefault()
if (!validInput()) return
const { error } = await supabase.auth.signUp({
email: email.trim(),
password,
})
if (error) {
setMessage(error.message)
return
}
setMessage('Registrasi berhasil. Periksa email untuk konfirmasi jika diwajibkan.')
}
async function signIn(event: FormEvent) {
event.preventDefault()
if (!validInput()) return
const { error } = await supabase.auth.signInWithPassword({
email: email.trim(),
password,
})
if (error) {
setMessage('Email atau password tidak valid.')
return
}
router.replace('/dashboard')
router.refresh()
}
async function signOut() {
const { error } = await supabase.auth.signOut()
if (error) {
setMessage(error.message)
return
}
router.replace('/login')
router.refresh()
}
return (
<form>
<input
type="email"
value={email}
onChange={(event) => setEmail(event.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="Password"
minLength={8}
required
/>
<button type="button" onClick={signIn}>Masuk</button>
<button type="button" onClick={signUp}>Daftar</button>
<button type="button" onClick={signOut}>Keluar</button>
<p>{message}</p>
</form>
)
}
Pada aplikasi nyata, tombol logout biasanya ditempatkan pada komponen navigasi yang hanya ditampilkan ketika pengguna sudah login. Untuk aturan password, rate limiting, atau proses yang membutuhkan secret, pertimbangkan Server Action atau Route Handler agar logika penting tidak hanya berada di browser.
5. OAuth callback di App Router
Alur OAuth biasanya terdiri dari dua tahap: browser diarahkan ke provider, lalu provider mengembalikan parameter code ke callback aplikasi. Callback harus menukar code tersebut menjadi session melalui exchangeCodeForSession.
Memulai OAuth
'use client'
import { createClient } from '@/lib/supabase/client'
export function GoogleButton() {
const supabase = createClient()
async function signInWithGoogle() {
const redirectTo = `${window.location.origin}/auth/callback`
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo },
})
if (error) console.error(error.message)
}
return <button onClick={signInWithGoogle}>Masuk dengan Google</button>
}
Membuat Route Handler callback
Buat file app/auth/callback/route.ts:
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: Request) {
const url = new URL(request.url)
const code = url.searchParams.get('code')
const requestedNext = url.searchParams.get('next')
const next = requestedNext?.startsWith('/') ? requestedNext : '/dashboard'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
return NextResponse.redirect(new URL(next, url.origin))
}
}
return NextResponse.redirect(new URL('/auth/auth-code-error', url.origin))
}
Pemeriksaan startsWith('/') mencegah redirect terbuka ke domain lain. Jangan langsung menggunakan nilai next dari query string tanpa validasi.
6. Memperbarui session dengan middleware
Cookie session dapat berubah ketika token perlu diperbarui. Middleware membantu meneruskan request ke Supabase dan menulis cookie terbaru ke response. Buat lib/supabase/middleware.ts:
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let response = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => {
request.cookies.set(name, value)
})
response = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) => {
response.cookies.set(name, value, options)
})
},
},
}
)
// getUser memvalidasi user terhadap Auth server dan dapat memicu refresh token.
await supabase.auth.getUser()
return response
}
Hubungkan middleware tersebut melalui middleware.ts di root project:
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/middleware'
export async function middleware(request: NextRequest) {
return updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
Matcher dapat dipersempit agar middleware hanya berjalan pada route yang membutuhkan session. Hindari mengecualikan route callback atau route lain yang memang membutuhkan pembaruan cookie.
7. Proteksi halaman di server
Middleware membantu memperbarui session, tetapi pemeriksaan otorisasi tetap perlu dilakukan pada halaman atau handler yang dilindungi. Gunakan getUser() ketika keputusan keamanan bergantung pada identitas pengguna karena hasilnya divalidasi ke server Supabase.
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
export default async function DashboardPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login?next=/dashboard')
}
return (
<main>
<h1>Dashboard</h1>
<p>Selamat datang, {user.email}</p>
</main>
)
}
Jangan menganggap halaman yang tersembunyi dari navigasi sebagai halaman yang terlindungi. Route Handler, Server Action, dan endpoint API juga harus memeriksa user serta otorisasi resource yang diminta.
8. Session di server dan browser
Di server
Server Component dapat memanggil supabase.auth.getUser() untuk memperoleh user berdasarkan cookie request. Server dapat mengakses data sensitif tanpa mengekspos credential database ke browser. Pastikan query data juga menerapkan Row Level Security (RLS), bukan hanya mengandalkan pemeriksaan pada UI.
Di browser
Browser client memantau perubahan autentikasi menggunakan onAuthStateChange. Ini berguna untuk memperbarui navigasi setelah login atau logout:
'use client'
import { useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
export function AuthListener() {
useEffect(() => {
const supabase = createClient()
const { data: subscription } = supabase.auth.onAuthStateChange(() => {
// Refresh Server Component yang bergantung pada session.
window.location.reload()
})
return () => subscription.subscription.unsubscribe()
}, [])
return null
}
Gunakan listener seperlunya. Pada aplikasi yang kompleks, router.refresh() atau state management terpusat biasanya lebih baik daripada memuat ulang seluruh halaman.
9. Konfigurasi redirect dan keamanan
- Redirect URL: tambahkan URL callback lokal dan production pada konfigurasi URL Supabase, misalnya
http://localhost:3000/auth/callbackdan URL HTTPS production. Jangan mengandalkan wildcard yang terlalu luas. - Cookie: biarkan
@supabase/ssrmengelola cookie session. Jangan menaruh access token secara manual dilocalStorageuntuk menggantikan integrasi cookie SSR. - HTTPS: gunakan HTTPS di production agar cookie dan credential tidak dikirim melalui koneksi yang tidak aman.
- Service role key: hanya gunakan di server pada proses administratif yang benar-benar membutuhkan hak istimewa. Key ini melewati RLS dan tidak boleh masuk ke browser.
- RLS: aktifkan Row Level Security pada tabel aplikasi dan buat policy berdasarkan
auth.uid(). Login saja tidak otomatis membatasi akses ke semua data. - Input dan pesan error: validasi format, panjang, dan tipe input. Hindari menampilkan detail internal atau membocorkan apakah email tertentu terdaftar pada alur yang sensitif.
- Open redirect: validasi parameter tujuan redirect dan izinkan hanya path internal atau daftar domain yang dipercaya.
10. Kesalahan umum dan cara debugging
- Session hilang setelah login: periksa callback URL, implementasi
setAll, dan apakah middleware mengembalikan response yang memuat cookie terbaru. - OAuth gagal dengan redirect mismatch: samakan URL yang dikirim pada
redirectTodengan URL yang didaftarkan di Supabase dan dashboard provider OAuth. - Environment variable undefined: pastikan nama variable tepat, file berada di root project, dan server development sudah direstart.
- Halaman tetap menampilkan user lama: panggil
router.refresh()setelah login atau logout agar Server Component dirender ulang berdasarkan cookie terbaru. - Data dapat diakses user lain: periksa policy RLS dan jangan hanya memfilter data berdasarkan parameter dari client.
- Redirect berulang: pastikan route login dan callback tidak ikut diproteksi secara keliru, serta middleware tidak melakukan redirect tanpa kondisi user yang jelas.
Kesimpulan
Integrasi Supabase Auth dengan Next.js App Router menjadi lebih konsisten jika browser client dan server client dipisahkan, cookie session diperbarui melalui middleware, dan callback OAuth menukar code pada Route Handler. Proteksi sebenarnya tetap harus dilakukan di server untuk setiap halaman, action, dan endpoint yang sensitif.
Gunakan getUser() untuk keputusan otorisasi, validasi input dan redirect, simpan secret hanya di server, serta lengkapi autentikasi dengan RLS. Dengan pola tersebut, session dapat digunakan baik pada rendering server maupun interaksi browser tanpa memindahkan token secara manual.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!