Next.js 14 Auth dengan Iron Session dan Server Actions

Autentikasi berbasis cookie pada router aplikasi Next.js dengan iron session dan server action

Next.js 14 Auth dengan Iron Session dan Server Actions

Autentikasi dan autorisasi sangat penting untuk aplikasi apa pun agar tetap aman dan berfungsi dengan baik. Bahkan kelalaian sekecil apa pun dapat menyebabkan masalah yang signifikan.

Ketika berbicara tentang Next.js, Auth.js adalah pilihan populer untuk menangani autentikasi. Namun, dokumentasinya terlalu rumit, sering kali membuat para developer bingung. Ini dikelola oleh sebuah tim kecil, yang berjuang dengan tantangan pemeliharaan dan maintenance. Mereka belum memigrasikan library next-auth yang lama ke auth.js dan versi yang kompatibel dengan fitur-fitur Next.js yang baru masih dalam versi beta.

Jika Anda ragu untuk menggunakan library pihak ketiga yang kompleks seperti Auth.js atau layanan autentikasi berbayar seperti Clerk, mari jelajahi metode yang lebih sederhana untuk autentikasi pengguna dengan menggunakan library ‘iron-session’.

Library ini menyederhanakan proses dengan membuat cookie yang signed dan encrypted, untuk memfasilitasi pengelolaan sesi pengguna dengan mudah. Dengan enkripsi dan dekripsi server-side, library ini memprioritaskan keamanan dan integritas data.

Mari kita lihat bagaimana cara menggunakannya untuk mengelola sesi, dengan memanfaatkan fitur-fitur terbaru seperti App router, React server components, dan server actions.

Pertama, kita perlu menginstal library-nya.

npm install iron-session

Sebelum masuk ke dalam pembuatan component, mari kita bahas opsi session dan server action. Kita akan membutuhkan tiga aksi utama untuk mengelola sesi secara efektif: aksi login untuk membuat sesi yang signed, aksi logout untuk mengakhiri sesi, dan aksi getSession untuk mendapatkan sesi pengguna untuk tujuan validasi.

Mari kita buat file bernama lib.ts dan tentukan tipe data session dan cookie options.

import { SessionOptions } from "iron-session";

export interface SessionData {
  userId?: string;
  username?: string;
  img?: string;
  isLoggedIn: boolean;
}

export const defaultSession: SessionData = {
  isLoggedIn: false,
};

export const sessionOptions: SessionOptions = {
  // You need to create a secret key at least 32 characters long.
  password: process.env.SESSION_SECRET!,
  cookieName: "rafa-session",
  cookieOptions: {
    httpOnly: true,
    // Secure only works in `https` environments. So if the environment is `https`, it'll return true.
    secure: process.env.NODE_ENV === "production",
  },
};

Saya lebih suka menggunakan user id, username dan user image untuk data pengguna, tetapi jangan ragu untuk menyimpan informasi tambahan apa pun yang Anda anggap perlu. Setelah user login, kami akan mengambil detail user dari database dan menyimpannya di dalam session.

Kemudian saya membuat opsi pada session. Ini akan mengenkripsi sesi dengan kunci (secret key) yang diberikan.

Anda dapat membuat secret key dengan menggunakan kode ini pada terminal Anda.

openssl rand -base64 32

Setelah itu kita siap untuk membuat action pertama. Buat sebuah file bernama actions.ts dan tambahkan kode berikut.

"use server"

import { SessionData } from "@/lib";
import { defaultSession, sessionOptions } from "@/lib";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

Kita sudah membuat SessionData, defaultSession dan sessionOptions di langkah sebelumnya. Kita juga membutuhkan cookie Next.js untuk mendapatkan session dari pengguna, fungsi getIronSession untuk mendekripsi session dengan cookie dan opsi session yang disediakan, dan fungsi redirect untuk mengarahkan pengguna ke homepage setelah proses login/logout.

Mari kita tambahkan login action.

"use server"

import { SessionData } from "@/lib";
import { defaultSession, sessionOptions } from "@/lib";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

// ADD THE LOGIN ACTION
export async function login(
  formData: FormData
) {
  const session = await getSession();

  const formUsername = formData.get("username") as string;
  const formPassword = formData.get("password") as string;

  // CHECK USER IN DB USING THE USERNAME AND PASSWORD
  // it depends on your database (mongoose,prisma,drizzle etc.)
  // for the testing purpose, I assigned a dummy user
  const user = {
    id:1,
    username:formUsername,
    img:"avatar.png"
  }

  // IF CREDENTIALS ARE WRONG RETURN AN ERROR
  if(!user){
    return { error: "Wrong Credentials!" }
  }

  // You can pass any information you want
  session.isLoggedIn = true;
  session.id = user.id;
  session.username = user.username;

  await session.save();
  redirect("/")
}

Pada contoh di atas, kita mengambil username dan password dari form client dan mencari user berdasarkan kredensial yang diberikan. Jika pengguna tidak ada dalam database atau kredensial salah, kami mengembalikan kesalahan (yang akan ditampilkan kepada user menggunakan hook useFormState). Jika tidak ada kesalahan yang terjadi, kita meneruskan informasi yang relevan ke session dan menyimpannya menggunakan metode save(). Ini mengenkripsi session dan mengirimkannya ke cookie user. Terakhir, Anda dapat mengarahkan user ke homepage.

Sekarang, kita dapat membuat aksi getSession untuk decrypt cookie user dan mengakses informasi user.

"use server"

import { SessionData } from "@/lib";
import { defaultSession, sessionOptions } from "@/lib";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

// ADD THE GETSESSION ACTION
export async function getSession() {
  const session = await getIronSession<SessionData>(cookies(), sessionOptions);

  // If user visits for the first time session returns an empty object.
  // Let's add the isLoggedIn property to this object and its value will be the default value which is false
  if (!session.isLoggedIn) {
    session.isLoggedIn = defaultSession.isLoggedIn;
  }

  return session;
}

export async function login(
  formData: FormData
) {
  const session = await getSession();

  const formUsername = formData.get("username") as string;
  const formPassword = formData.get("password") as string;

  const user = {
    id:1,
    username:formUsername,
    img:"avatar.png"
  }

  if(!user){
    return { error: "Wrong Credentials!" }
  }

  session.isLoggedIn = true;
  session.id = user.id;
  session.username = user.username;

  await session.save();
  redirect("/")
}

Mari kita mengujinya dengan membuat form. Membuat login page dan component form

import { getSession } from "@/actions";
import LoginForm from "@/components/loginForm";
import { redirect } from "next/navigation";

const LoginPage = async () => {
  const session = await getSession();

  if (session.isLoggedIn) {
    redirect("/");
  }

  return (
    <div>
      <h1>Login Page</h1>
      <LoginForm />
    </div>
  );
};

export default LoginPage;
"use client";
import { login } from "@/actions";
import { useFormState } from "react-dom";

export default function LoginForm() {
  const [state, formAction] = useFormState<any, FormData>(login, undefined);
  return (
    <form action={formAction}>
      <input type="text" placeholder="username" name="username" />
      <input type="password" placeholder="password" name="password" />
      <button>Login</button>
      {state?.error}
    </form>
  );
}

Pada login page, pertama-tama kita akan mengambil sesi menggunakan fungsi getSession. Jika kita sudah login, kita akan diarahkan ke homepage menggunakan fungsi redirect. Namun, karena kita belum memiliki sesi, kita akan tetap berada di halaman ini.

Pada form action, Anda dapat mengirimkan login action secara langsung. Namun, seperti yang telah saya sebutkan, jika terjadi kesalahan, kita akan menampilkannya kepada user dengan menggunakan hook useFormState. Pada awalnya, status error tidak terdefinisi. Jika terjadi kesalahan, kita akan memperbarui state dalam login function. Untuk mencapai hal ini, mari kita kembali ke login function dan menambahkan satu parameter lagi.

export async function login(
  // THIS IS THE PARAMETER THAT WE NEED TO ADD
  prevState: { error: undefined | string },
  formData: FormData
) {
  const session = await getSession();

  const formUsername = formData.get("username") as string;
  const formPassword = formData.get("password") as string;

  const user = {
    id:1,
    username:formUsername,
    img:"avatar.png"
  }

  if(!user){
    // IF THERE IS AN ERROR THE STATE WILL BE UPDATED
    return { error: "Wrong Credentials!" }
  }

  session.isLoggedIn = true;
  session.id = user.id;
  session.username = user.username;

  await session.save();
  redirect("/")
}

Saat ini, dengan mengklik tombol submit akan membuat Anda masuk dan mengarahkan Anda ke homepage. Saat ini, Anda tidak dapat melihat halaman login, karena kita sudah masuk, dan akan diarahkan ke beranda secara otomatis.

Untuk menguji pesan kesalahan, Anda dapat mendefinisikan user null pada login action.

Sekarang, mari kita lanjutkan ke file actions.ts dan membuat logout function. Di dalam fungsi ini, kita akan menjalankan proses untuk mengakhiri session.

"use server"

import { SessionData } from "@/lib";
import { defaultSession, sessionOptions } from "@/lib";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

// ADD THE LOGOUT FUNCTION
export async function logout() {
  const session = await getSession();
  session.destroy();
  redirect("/")
}

export async function getSession() {
  const session = await getIronSession<SessionData>(cookies(), sessionOptions);

  if (!session.isLoggedIn) {
    session.isLoggedIn = defaultSession.isLoggedIn;
  }

  return session;
}

export async function login(
  formData: FormData
) {
  const session = await getSession();

  const formUsername = formData.get("username") as string;
  const formPassword = formData.get("password") as string;

  const user = {
    id:1,
    username:formUsername,
    img:"avatar.png"
  }

  if(!user){
    return { error: "Wrong Credentials!" }
  }

  session.isLoggedIn = true;
  session.id = user.id;
  session.username = user.username;

  await session.save();
  redirect("/")
}

Untuk menguji aksi ini, mari kita buat form logout.

import { logout } from "@/actions";

export default function LogoutForm() {
  return (
    <form action={logout}>
      <button>Logout</button>
    </form>
  );
}

Dan akhirnya selesai! Metode ini menggunakan pendekatan langsung untuk membuat dan mengelola cookie autentikasi dengan aman.