Web Development

NextAuth.js: Authentication untuk Next.js

Tutorial lengkap NextAuth.js β€” providers, sessions, callbacks, database adapters, OAuth integration, credentials login, JWT strategy, dan best practices keamanan

1. Pengenalan NextAuth.js

NextAuth.js (sekarang bernama Auth.js) adalah library autentikasi open-source yang dirancang khusus untuk aplikasi Next.js. Library ini menyediakan solusi autentikasi yang lengkap β€” mulai dari OAuth social login (Google, GitHub, Facebook, dll.), credentials-based login, hingga manajemen sesi β€” dengan konfigurasi minimal.

Dibuat oleh Iain Collins, NextAuth.js telah menjadi standar de facto untuk autentikasi di ekosistem Next.js. Dengan lebih dari 20.000+ stars di GitHub dan digunakan oleh ribuan aplikasi production.

Fitur Utama NextAuth.js

Fitur Penjelasan
OAuth ProvidersDukungan 80+ provider: Google, GitHub, Facebook, Twitter, Apple, dll.
Email/PasswordSupport credentials-based login (username/password)
Magic LinkLogin via email tanpa password (passwordless)
Session StrategyJWT (default) atau database sessions
Database AdaptersPrisma, Drizzle, MongoDB, Fauna, Supabase, dll.
CallbacksKustomisasi alur autentikasi: sign-in, session, JWT, redirect
CSRF ProtectionPerlindungan CSRF built-in
Secure CookiesHttpOnly, Secure, SameSite cookies otomatis

Arsitektur NextAuth.js

Diagram: Arsitektur NextAuth.js
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   CLIENT (Browser)                        β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ signIn()     β”‚   β”‚ useSession() β”‚   β”‚ signOut()   β”‚  β”‚
β”‚  β”‚ Login/Logout β”‚   β”‚ Get Session  β”‚   β”‚ Logout      β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚         β”‚                  β”‚                   β”‚          β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚                            β”‚                              β”‚
β”‚                   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚                   β”‚  Secure Cookies β”‚                     β”‚
β”‚                   β”‚  (HttpOnly,     β”‚                     β”‚
β”‚                   β”‚   SameSite)     β”‚                     β”‚
β”‚                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Next.js Server                          β”‚
β”‚                      β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”                        β”‚
β”‚                      β”‚ NextAuth  β”‚                        β”‚
β”‚                      β”‚ API Route β”‚                        β”‚
β”‚                      β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                        β”‚
β”‚                            β”‚                              β”‚
β”‚       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”‚
β”‚  β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ OAuth    β”‚    β”‚    Callbacks     β”‚   β”‚  Database   β”‚  β”‚
β”‚  β”‚ Providersβ”‚    β”‚  (jwt, session,  β”‚   β”‚  Adapter    β”‚  β”‚
β”‚  β”‚ (Google, β”‚    β”‚   signIn, redirectβ”‚  β”‚  (Prisma,   β”‚  β”‚
β”‚  β”‚  GitHub) β”‚    β”‚   )              β”‚   β”‚   MongoDB)  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Instalasi & Setup

Mari kita mulai dengan menginstal dan mengkonfigurasi NextAuth.js di proyek Next.js. Pastikan Anda sudah memiliki proyek Next.js yang berjalan.

Instalasi

Bash
# Instal NextAuth.js
npm install next-auth

# Untuk database adapter (opsional, jika menggunakan database)
npm install @prisma/client prisma @auth/prisma-adapter

# Untuk Next.js App Router (v13+)
# Sudah termasuk dalam next-auth@latest

Konfigurasi Environment Variables

.env.local
# NextAuth.js
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=rahasia-super-panjang-dan-acak-untuk-produksi

# Google OAuth (dapatkan dari console.cloud.google.com)
GOOGLE_CLIENT_ID=xxxxxxxxxxxx-xxxxxxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxx

# GitHub OAuth (dapatkan dari github.com/settings/developers)
GITHUB_CLIENT_ID=xxxxxxxxxxxxxxxxxxxx
GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Database URL (jika menggunakan Prisma)
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
⚠️ NEXTAUTH_SECRET

NEXTAUTH_SECRET adalah string acak yang panjang (minimal 32 karakter). Ini digunakan untuk mengenkripsi JWT tokens dan cookies. Di production, gunakan openssl rand -base64 32 untuk generate secret yang aman.

API Route β€” NextAuth Configuration

JavaScript β€” app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import GitHubProvider from 'next-auth/providers/github';
import CredentialsProvider from 'next-auth/providers/credentials';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';
import bcrypt from 'bcryptjs';

const handler = NextAuth({
  // Adapter untuk menyimpan user/session di database
  adapter: PrismaAdapter(prisma),

  // Authentication providers
  providers: [
    // Google OAuth
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      authorization: {
        params: {
          prompt: 'consent',
          access_type: 'offline',
          response_type: 'code',
        },
      },
    }),

    // GitHub OAuth
    GitHubProvider({
      clientId: process.env.GITHUB_CLIENT_ID,
      clientSecret: process.env.GITHUB_CLIENT_SECRET,
    }),

    // Credentials (username/password)
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: {
          label: 'Email',
          type: 'email',
          placeholder: 'email@kamu.com',
        },
        password: {
          label: 'Password',
          type: 'password',
        },
      },
      async authorize(credentials) {
        // Cari user di database
        const user = await prisma.user.findUnique({
          where: { email: credentials.email },
        });

        if (!user || !user.hashedPassword) {
          throw new Error('Email atau password salah');
        }

        // Verifikasi password
        const isValid = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        );

        if (!isValid) {
          throw new Error('Email atau password salah');
        }

        // Kembalikan user object (akan masuk ke jwt callback)
        return {
          id: user.id,
          name: user.name,
          email: user.email,
          image: user.image,
          role: user.role,
        };
      },
    }),
  ],

  // Session strategy
  session: {
    strategy: 'jwt', // 'jwt' atau 'database'
    maxAge: 30 * 24 * 60 * 60, // 30 hari
    updateAge: 24 * 60 * 60, // 24 jam
  },

  // Halaman custom
  pages: {
    signIn: '/auth/login',       // Custom login page
    signOut: '/auth/logout',     // Custom logout page
    error: '/auth/error',        // Error page
    newUser: '/auth/register',   // Redirect new user setelah sign up
  },

  // Callbacks
  callbacks: {
    // 1. Sign In Callback β€” kontrol apakah user boleh login
    async signIn({ user, account, profile }) {
      if (account.provider === 'google') {
        // Contoh: hanya izinkan email tertentu
        // return profile.email.endsWith('@beebane.com');
        return true;
      }
      if (account.provider === 'credentials') {
        return !!user; // Pastikan authorize() mengembalikan user
      }
      return true;
    },

    // 2. JWT Callback β€” kustomisasi isi JWT token
    async jwt({ token, user, account, profile, trigger }) {
      // Saat pertama kali sign in
      if (user) {
        token.id = user.id;
        token.role = user.role || 'user';
      }

      // Refresh token saat session di-update
      if (trigger === 'update') {
        const refreshedUser = await prisma.user.findUnique({
          where: { id: token.id },
        });
        token.role = refreshedUser.role;
      }

      return token;
    },

    // 3. Session Callback β€” kustomisasi data yang dikirim ke client
    async session({ session, token }) {
      if (token) {
        session.user.id = token.id;
        session.user.role = token.role;
      }
      return session;
    },

    // 4. Redirect Callback β€” kustomisasi URL redirect
    async redirect({ url, baseUrl }) {
      // Redirect ke halaman yang diminta setelah login
      if (url.startsWith('/')) return `${baseUrl}${url}`;
      if (new URL(url).origin === baseUrl) return url;
      return baseUrl;
    },
  },

  // Events β€” hook untuk berbagai tahap autentikasi
  events: {
    async signIn({ user, account, profile }) {
      console.log(`User ${user.email} logged in via ${account.provider}`);
    },
    async signOut({ token }) {
      console.log(`User ${token.email} logged out`);
    },
    async createUser({ user }) {
      console.log(`New user created: ${user.email}`);
      // Kirim email selamat datang, dll.
    },
    async linkAccount({ user, account }) {
      console.log(`Account ${account.provider} linked to ${user.email}`);
    },
  },

  // Debug mode (matikan di production!)
  debug: process.env.NODE_ENV === 'development',
});

export { handler as GET, handler as POST };

Session Provider di Layout

JSX β€” app/layout.js
import { SessionProvider } from '@/components/SessionProvider';
import './globals.css';

export const metadata = {
  title: 'Aplikasi Saya',
  description: 'Dengan NextAuth.js authentication',
};

export default function RootLayout({ children }) {
  return (
    <html lang="id">
      <body>
        <SessionProvider>
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}
JSX β€” components/SessionProvider.js
'use client';

import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react';

export function SessionProvider({ children }) {
  return (
    <NextAuthSessionProvider>
      {children}
    </NextAuthSessionProvider>
  );
}

3. Authentication Providers

NextAuth.js mendukung berbagai authentication providers. Berikut adalah beberapa yang paling umum digunakan:

Google OAuth Provider

Setup Google OAuth
Langkah-langkah setup Google OAuth:

1. Buka https://console.cloud.google.com
2. Buat project baru atau pilih project yang ada
3. Aktifkan "Google+ API" di Library
4. Buka "Credentials" β†’ "Create Credentials" β†’ "OAuth client ID"
5. Pilih "Web application"
6. Tambahkan Authorized redirect URIs:
   - http://localhost:3000/api/auth/callback/google
   - https://domain-anda.com/api/auth/callback/google
7. Salin Client ID dan Client Secret ke .env.local

GitHub OAuth Provider

Setup GitHub OAuth
Langkah-langkah setup GitHub OAuth:

1. Buka https://github.com/settings/developers
2. Klik "New OAuth App"
3. Isi form:
   - Application name: Nama aplikasi Anda
   - Homepage URL: http://localhost:3000
   - Authorization callback URL: http://localhost:3000/api/auth/callback/github
4. Klik "Register application"
5. Generate Client Secret
6. Salin Client ID dan Client Secret ke .env.local

Menambah Provider Lain

JavaScript β€” Berbagai Providers
import GoogleProvider from 'next-auth/providers/google';
import GitHubProvider from 'next-auth/providers/github';
import FacebookProvider from 'next-auth/providers/facebook';
import TwitterProvider from 'next-auth/providers/twitter';
import AppleProvider from 'next-auth/providers/apple';
import DiscordProvider from 'next-auth/providers/discord';
import EmailProvider from 'next-auth/providers/email';

// Di dalam NextAuth config:
providers: [
  // OAuth Providers
  GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  }),

  GitHubProvider({
    clientId: process.env.GITHUB_CLIENT_ID,
    clientSecret: process.env.GITHUB_CLIENT_SECRET,
    // Scope tambahan untuk mengakses data lebih banyak
    // profile(profile) {
    //   return {
    //     id: profile.id.toString(),
    //     name: profile.name || profile.login,
    //     email: profile.email,
    //     image: profile.avatar_url,
    //   };
    // },
  }),

  FacebookProvider({
    clientId: process.env.FACEBOOK_CLIENT_ID,
    clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
  }),

  TwitterProvider({
    clientId: process.env.TWITTER_CLIENT_ID,
    clientSecret: process.env.TWITTER_CLIENT_SECRET,
    version: '2.0',
  }),

  AppleProvider({
    clientId: process.env.APPLE_CLIENT_ID,
    clientSecret: process.env.APPLE_CLIENT_SECRET,
  }),

  DiscordProvider({
    clientId: process.env.DISCORD_CLIENT_ID,
    clientSecret: process.env.DISCORD_CLIENT_SECRET,
  }),

  // Magic Link / Passwordless
  EmailProvider({
    server: {
      host: process.env.EMAIL_SERVER_HOST,
      port: process.env.EMAIL_SERVER_PORT,
      auth: {
        user: process.env.EMAIL_SERVER_USER,
        pass: process.env.EMAIL_SERVER_PASSWORD,
      },
    },
    from: process.env.EMAIL_FROM,
    // maxAge: 24 * 60 * 60, // Token berlaku 24 jam
  }),
],

Login Buttons Component

JSX β€” components/LoginButtons.jsx
'use client';

import { signIn } from 'next-auth/react';

function LoginButtons() {
  return (
    <div className="login-buttons">
      <button
        onClick={() => signIn('google', { callbackUrl: '/dashboard' })}
        className="btn-google"
      >
        πŸ”΅ Masuk dengan Google
      </button>

      <button
        onClick={() => signIn('github', { callbackUrl: '/dashboard' })}
        className="btn-github"
      >
        ⚫ Masuk dengan GitHub
      </button>

      <button
        onClick={() => signIn('facebook', { callbackUrl: '/dashboard' })}
        className="btn-facebook"
      >
        πŸ”· Masuk dengan Facebook
      </button>

      <div className="divider">atau</div>

      <a href="/auth/login" className="btn-credentials">
        πŸ“§ Masuk dengan Email & Password
      </a>
    </div>
  );
}

export default LoginButtons;

4. Session Management

NextAuth.js mendukung dua strategi session: JWT (default) dan Database. Pilihan ini mempengaruhi bagaimana data session disimpan dan diakses.

JWT vs Database Sessions

Aspek JWT Strategy Database Strategy
PenyimpananDi cookie (encrypted)Di database
Database Required?TidakYa
Server-side LogoutTidak bisa (token valid sampai expired)Bisa (hapus dari DB)
Performa🟒 Lebih cepat (tidak perlu DB query)🟑 Perlu query DB setiap request
Ukuran Token~400 bytes (di cookie)Hanya session token kecil
RekomendasiApp tanpa database adapterApp dengan database, perlu revoke session

Menggunakan Session di Komponen

JSX β€” components/Navbar.jsx
'use client';

import { useSession, signIn, signOut } from 'next-auth/react';

function Navbar() {
  const { data: session, status } = useSession();
  // status: 'loading' | 'authenticated' | 'unauthenticated'

  return (
    <nav className="navbar">
      <a href="/" className="logo">BeebaneLabs</a>

      <div className="nav-right">
        {status === 'loading' && (
          <span className="loading-text">Memuat...</span>
        )}

        {status === 'unauthenticated' && (
          <button onClick={() => signIn()}>
            Masuk
          </button>
        )}

        {status === 'authenticated' && session && (
          <div className="user-menu">
            <img
              src={session.user.image || '/default-avatar.png'}
              alt={session.user.name}
              className="avatar"
            />
            <span>{session.user.name}</span>
            {session.user.role === 'admin' && (
              <span className="badge-admin">Admin</span>
            )}
            <button onClick={() => signOut({ callbackUrl: '/' })}>
              Keluar
            </button>
          </div>
        )}
      </div>
    </nav>
  );
}

export default Navbar;

Server-side Session (App Router)

JavaScript β€” Server Components & API Routes
// Server Component β€” mendapatkan session di server
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { redirect } from 'next/navigation';

export default async function DashboardPage() {
  const session = await getServerSession(authOptions);

  // Redirect jika belum login
  if (!session) {
    redirect('/auth/login');
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Selamat datang, {session.user.name}!</p>
      <p>Role: {session.user.role}</p>
      <img src={session.user.image} alt="Avatar" />
    </div>
  );
}

// API Route β€” proteksi API endpoint
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import { NextResponse } from 'next/server';

export async function GET() {
  const session = await getServerSession(authOptions);

  if (!session) {
    return NextResponse.json(
      { error: 'Tidak terautentikasi' },
      { status: 401 }
    );
  }

  // Hanya admin yang boleh akses
  if (session.user.role !== 'admin') {
    return NextResponse.json(
      { error: 'Akses ditolak' },
      { status: 403 }
    );
  }

  // Lanjutkan proses...
  return NextResponse.json({ data: 'Data rahasia admin' });
}

5. Callbacks

Callbacks adalah fungsi yang dijalankan pada berbagai tahap proses autentikasi. Mereka memungkinkan Anda mengkustomisasi perilaku default NextAuth.js.

Empat Jenis Callbacks

JavaScript β€” Callbacks Detail
callbacks: {
  // ═══════════════════════════════════════════════
  // 1. signIn β€” kontrol apakah user boleh login
  // Dipanggil saat user mencoba sign in
  // Return true = izinkan, false/error = tolak
  // ═══════════════════════════════════════════════
  async signIn({ user, account, profile, email, credentials }) {
    // user: data user dari provider atau authorize()
    // account: info provider (provider, type, accessToken)
    // profile: profil dari provider OAuth
    // email: info verifikasi email (untuk Email provider)
    // credentials: data dari Credentials provider

    // Contoh: blacklist email tertentu
    const blacklist = ['spam@example.com'];
    if (blacklist.includes(user.email)) {
      return false; // Tolak login
    }

    // Contoh: hanya izinkan email verified dari Google
    if (account.provider === 'google' && !profile.email_verified) {
      return '/auth/error?error=EmailNotVerified';
    }

    return true; // Izinkan login
  },

  // ═══════════════════════════════════════════════
  // 2. redirect β€” kustomisasi URL setelah sign in/out
  // ═══════════════════════════════════════════════
  async redirect({ url, baseUrl }) {
    // url: URL yang diminta (dari callbackUrl parameter)
    // baseUrl: URL dasar aplikasi (NEXTAUTH_URL)

    // Default: redirect ke halaman yang diminta
    if (url.startsWith('/')) return `${baseUrl}${url}`;

    // Pastikan hanya redirect ke domain sendiri
    if (new URL(url).origin === baseUrl) return url;

    // Fallback ke homepage
    return baseUrl;
  },

  // ═══════════════════════════════════════════════
  // 3. jwt β€” kustomisasi isi JWT token
  // Hanya dipanggil jika strategy: 'jwt'
  // ═══════════════════════════════════════════════
  async jwt({ token, user, account, profile, trigger, isNewUser, session }) {
    // token: JWT token saat ini (persisted antar requests)
    // user: data user (hanya ada saat sign in pertama)
    // account: info provider (hanya ada saat sign in)
    // trigger: 'signIn', 'update', atau 'signUp'

    // Saat pertama kali sign in, tambahkan data ke token
    if (user) {
      token.id = user.id;
      token.role = user.role || 'user';
      token.provider = account?.provider;
    }

    // Saat session.update() dipanggil dari client
    if (trigger === 'update' && session) {
      token.name = session.name;
      token.image = session.image;
    }

    return token;
  },

  // ═══════════════════════════════════════════════
  // 4. session β€” kustomisasi data yang dikirim ke client
  // Dipanggil setiap kali useSession() atau getServerSession()
  // ═══════════════════════════════════════════════
  async session({ session, token, user }) {
    // session: objek session yang akan dikirim ke client
    // token: JWT token (jika strategy: 'jwt')
    // user: data user dari database (jika strategy: 'database')

    // Tambahkan data dari token ke session
    if (token) {
      session.user.id = token.id;
      session.user.role = token.role;
      session.user.provider = token.provider;
    }

    // Bisa juga tambahkan data dari database
    // const dbUser = await prisma.user.findUnique({
    //   where: { id: token.id }
    // });
    // session.user.subscription = dbUser.subscription;

    return session;
  },
},

Update Session dari Client

JSX β€” Update Session
'use client';

import { useSession } from 'next-auth/react';

function EditProfil() {
  const { data: session, update } = useSession();

  const handleUpdateNama = async (newName) => {
    // Update session β€” trigger jwt callback dengan trigger: 'update'
    await update({
      ...session,
      user: {
        ...session.user,
        name: newName,
      },
    });
    // Session akan di-refresh dengan data baru
  };

  return (
    <div>
      <p>Nama: {session?.user?.name}</p>
      <button onClick={() => handleUpdateNama('Nama Baru')}>
        Ubah Nama
      </button>
    </div>
  );
}

6. Database Adapters

Database Adapter memungkinkan NextAuth.js menyimpan data user, akun, session, dan verification token ke database. Ini diperlukan saat menggunakan database session strategy atau ketika Anda ingin menyimpan data user di database sendiri.

Prisma Adapter Setup

Prisma Schema β€” prisma/schema.prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Tabel ini dibuat otomatis oleh Prisma Adapter
model Account {
  id                String  @id @default(cuid())
  userId            String
  type              String
  provider          String
  providerAccountId String
  refresh_token     String? @db.Text
  access_token      String? @db.Text
  expires_at        Int?
  token_type        String?
  scope             String?
  id_token          String? @db.Text
  session_state     String?

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id           String   @id @default(cuid())
  sessionToken String   @unique
  userId       String
  expires      DateTime

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model User {
  id             String    @id @default(cuid())
  name           String?
  email          String?   @unique
  emailVerified  DateTime?
  image          String?
  hashedPassword String?   // Untuk credentials provider
  role           String    @default("user")
  createdAt      DateTime  @default(now())

  accounts Account[]
  sessions Session[]
}

model VerificationToken {
  identifier String
  token      String   @unique
  expires    DateTime

  @@unique([identifier, token])
}
JavaScript β€” lib/prisma.js
import { PrismaClient } from '@prisma/client';

// Prevent multiple instances in development
const globalForPrisma = globalThis;

export const prisma = globalForPrisma.prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}
πŸ’‘ Prisma Commands

Setelah membuat schema, jalankan:

  • npx prisma migrate dev β€” buat migration dan update database
  • npx prisma generate β€” generate Prisma Client
  • npx prisma studio β€” buka visual database editor di browser

7. Credentials Provider

Credentials Provider memungkinkan login dengan email dan password. Ini berguna ketika Anda ingin sistem autentikasi sendiri selain OAuth.

⚠️ Peringatan Credentials Provider

NextAuth.js merekomendasikan OAuth providers karena lebih aman. Credentials Provider tidak mendukung database sessions dan fitur tertentu seperti account linking. Gunakan hanya jika benar-benar diperlukan.

Register & Login Flow

JSX β€” app/auth/register/page.js
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';

function RegisterPage() {
  const [formData, setFormData] = useState({
    name: '', email: '', password: '', confirmPassword: '',
  });
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');

    if (formData.password !== formData.confirmPassword) {
      setError('Password tidak cocok');
      return;
    }

    if (formData.password.length < 8) {
      setError('Password minimal 8 karakter');
      return;
    }

    setLoading(true);
    try {
      const res = await fetch('/api/auth/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: formData.name,
          email: formData.email,
          password: formData.password,
        }),
      });

      const data = await res.json();

      if (!res.ok) {
        throw new Error(data.error || 'Gagal mendaftar');
      }

      // Redirect ke login setelah berhasil
      router.push('/auth/login?registered=true');
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="auth-page">
      <h1>Daftar Akun Baru</h1>

      {error && <p className="error">{error}</p>}

      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>Nama Lengkap</label>
          <input
            type="text"
            value={formData.name}
            onChange={(e) => setFormData({ ...formData, name: e.target.value })}
            required
          />
        </div>

        <div className="form-group">
          <label>Email</label>
          <input
            type="email"
            value={formData.email}
            onChange={(e) => setFormData({ ...formData, email: e.target.value })}
            required
          />
        </div>

        <div className="form-group">
          <label>Password</label>
          <input
            type="password"
            value={formData.password}
            onChange={(e) => setFormData({ ...formData, password: e.target.value })}
            required
            minLength={8}
          />
        </div>

        <div className="form-group">
          <label>Konfirmasi Password</label>
          <input
            type="password"
            value={formData.confirmPassword}
            onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
            required
          />
        </div>

        <button type="submit" disabled={loading}>
          {loading ? 'Mendaftar...' : 'πŸ“ Daftar'}
        </button>
      </form>

      <p className="auth-link">
        Sudah punya akun? <a href="/auth/login">Masuk di sini</a>
      </p>
    </div>
  );
}

export default RegisterPage;

Register API Route

JavaScript β€” app/api/auth/register/route.js
import { NextResponse } from 'next/server';
import bcrypt from 'bcryptjs';
import { prisma } from '@/lib/prisma';

export async function POST(request) {
  try {
    const { name, email, password } = await request.json();

    // Validasi input
    if (!name || !email || !password) {
      return NextResponse.json(
        { error: 'Semua field wajib diisi' },
        { status: 400 }
      );
    }

    if (password.length < 8) {
      return NextResponse.json(
        { error: 'Password minimal 8 karakter' },
        { status: 400 }
      );
    }

    // Cek apakah email sudah terdaftar
    const existingUser = await prisma.user.findUnique({
      where: { email },
    });

    if (existingUser) {
      return NextResponse.json(
        { error: 'Email sudah terdaftar' },
        { status: 409 }
      );
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 12);

    // Buat user baru
    const user = await prisma.user.create({
      data: {
        name,
        email,
        hashedPassword,
        role: 'user',
      },
    });

    return NextResponse.json(
      {
        message: 'Registrasi berhasil',
        user: { id: user.id, name: user.name, email: user.email },
      },
      { status: 201 }
    );
  } catch (error) {
    console.error('Register error:', error);
    return NextResponse.json(
      { error: 'Terjadi kesalahan server' },
      { status: 500 }
    );
  }
}

8. Protecting Routes

NextAuth.js mendukung proteksi route menggunakan Next.js Middleware. Ini memungkinkan Anda melindungi halaman dan API routes dari akses yang tidak sah.

Middleware untuk Auth Protection

JavaScript β€” middleware.js (root project)
import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';

// Proteksi semua route yang cocok dengan matcher
export default withAuth(
  function middleware(req) {
    const { token } = req.nextauth;
    const { pathname } = req.nextUrl;

    // Proteksi route admin β€” hanya untuk role admin
    if (pathname.startsWith('/admin') && token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/unauthorized', req.url));
    }

    // Proteksi route premium β€” untuk role premium atau admin
    if (pathname.startsWith('/premium') &&
        !['admin', 'premium'].includes(token?.role)) {
      return NextResponse.redirect(new URL('/upgrade', req.url));
    }

    return NextResponse.next();
  },
  {
    callbacks: {
      // Return true = izinkan akses, false = redirect ke signIn
      authorized: ({ token }) => {
        return !!token; // User harus terautentikasi
      },
    },
  }
);

// Tentukan route mana yang dilindungi
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/admin/:path*',
    '/premium/:path*',
    '/settings/:path*',
    '/api/protected/:path*',
  ],
};

HOC untuk Client-side Protection

JavaScript β€” lib/withAuth.jsx
'use client';

import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

// Higher-Order Component untuk proteksi halaman
export function withAuth(WrappedComponent, requiredRole = null) {
  return function ProtectedComponent(props) {
    const { data: session, status } = useSession();
    const router = useRouter();

    useEffect(() => {
      if (status === 'unauthenticated') {
        router.push('/auth/login');
      }

      if (status === 'authenticated' && requiredRole) {
        if (session.user.role !== requiredRole &&
            session.user.role !== 'admin') {
          router.push('/unauthorized');
        }
      }
    }, [status, session, router]);

    if (status === 'loading') {
      return <div className="loading">Memuat...</div>;
    }

    if (!session) return null;

    return <WrappedComponent {...props} session={session} />;
  };
}

// Penggunaan:
// const ProtectedDashboard = withAuth(DashboardPage);
// const AdminPage = withAuth(AdminPanel, 'admin');

9. Best Practices & Keamanan

Checklist Keamanan

Aspek Best Practice
NEXTAUTH_SECRETGunakan string acak minimal 32 karakter, simpan di env vars
CSRFNextAuth.js menangani CSRF otomatis β€” jangan disable
HTTPSSelalu gunakan HTTPS di production
Password HashingGunakan bcrypt dengan salt rounds β‰₯ 12
Session Max AgeAtur maxAge sesuai kebutuhan (default 30 hari)
Rate LimitingTerapkan rate limiting pada endpoint login/register
Input ValidationValidasi semua input di server-side
Error MessagesJangan reveal info sensitif di error message
πŸ’‘ Tips Production
  • Matikan debug: true di production
  • Gunakan NEXTAUTH_URL yang benar (https://domain-anda.com)
  • Monitoring: log semua sign-in events untuk audit trail
  • Implementasikan rate limiting (misal: 5 attempts per 15 menit)
  • Gunakan HTTP-only cookies (default NextAuth.js)
  • Pertimbangkan 2FA untuk akun admin

10. Quiz Pemahaman

Uji pemahaman Anda tentang NextAuth.js:

1. Apa perbedaan utama antara JWT dan Database session strategy?

2. Callback apa yang digunakan untuk menambahkan role ke session?

3. Mengapa NEXTAUTH_SECRET penting?

4. Di mana NextAuth API route ditempatkan di Next.js App Router?

5. Fungsi apa yang digunakan untuk mendapatkan session di server (App Router)?

πŸ” Zoom
100%
🎨 Tema