Bài 31.1: Implementing NextAuth.js

Chào mừng quay trở lại với hành trình khám phá Next.js! Hôm nay, chúng ta sẽ lặn sâu vào một chủ đề cực kỳ quan trọng đối với mọi ứng dụng web hiện đại: Xác thực và phân quyền (Authentication & Authorization). Và công cụ mạnh mẽ chúng ta sẽ khai thác chính là NextAuth.js – giải pháp xác thực tối ưu cho Next.js, giúp bạn dễ dàng thêm tính năng đăng nhập/đăng ký bằng nhiều phương thức khác nhau chỉ với vài bước đơn giản.

Tại Sao Lại Là NextAuth.js?

Xây dựng hệ thống xác thực từ đầu là một công việc phức tạp và tiềm ẩn nhiều rủi ro bảo mật. Bạn phải xử lý mã hóa mật khẩu, quản lý phiên, xử lý OAuth với các nhà cung cấp khác nhau (Google, GitHub, Facebook...), xử lý đăng nhập bằng email (magic links), và còn nhiều thứ khác nữa. NextAuth.js ra đời để giải quyết những vấn đề này. Nó cung cấp một layer abstraction mạnh mẽ, ẩn đi sự phức tạp bên dưới và cho phép bạn tập trung vào việc xây dựng ứng dụng của mình.

NextAuth.js hỗ trợ:

  • Nhiều loại Providers (OAuth, Email, Credentials).
  • Quản lý session (phiên đăng nhập) linh hoạt (JWT hoặc Database).
  • Dễ dàng tích hợp với các cơ sở dữ liệu phổ biến thông qua Adapters.
  • Bảo mật cao.
  • Hỗ trợ cả Server-Side Rendering (SSR) và Client-Side Rendering (CSR).

Hãy cùng bắt tay vào triển khai NextAuth.js trong dự án Next.js của chúng ta!

Bước 1: Cài Đặt NextAuth.js

Việc đầu tiên cần làm là thêm NextAuth.js vào project của bạn. Mở terminal trong thư mục gốc của dự án và chạy lệnh sau:

npm install next-auth
# hoặc
yarn add next-auth

Lệnh này sẽ tải và cài đặt thư viện next-auth cùng các dependency cần thiết.

Bước 2: Thiết Lập API Route Cho NextAuth.js

Điểm mấu chốt của NextAuth.js nằm ở một API route đặc biệt. Route này sẽ xử lý tất cả các yêu cầu liên quan đến xác thực, như đăng nhập, đăng xuất, callback từ các nhà cung cấp OAuth, v.v.

Bạn cần tạo một file trong thư mục pages/api/auth. Tên file phải là [...nextauth].js (hoặc [...nextauth].ts nếu bạn dùng TypeScript). Dấu ba chấm ... ở đầu tên file chỉ ra rằng đây là một catch-all route, Next.js sẽ bắt mọi request đến /api/auth/* và xử lý chúng bằng file này.

Đường dẫn file sẽ là: pages/api/auth/[...nextauth].js

Nội dung ban đầu của file này sẽ trông như thế này:

// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
// import Providers from "next-auth/providers"; // Cú pháp cũ
// Import Providers theo cú pháp mới
// import GoogleProvider from "next-auth/providers/google";
// import GitHubProvider from "next-auth/providers/github";
// import CredentialsProvider from "next-auth/providers/credentials";

export default NextAuth({
  // Cấu hình các nhà cung cấp xác thực (Providers) tại đây
  providers: [
    // Ví dụ: GoogleProvider({ ... })
    // Ví dụ: GitHubProvider({ ... })
    // Ví dụ: CredentialsProvider({ ... })
  ],

  // Tuỳ chọn cấu hình khác (session, callbacks, pages, database,...)
  session: {
    // Chiến lược quản lý session: "jwt" hoặc "database"
    strategy: "jwt",
  },

  // Cấu hình Adapter nếu sử dụng session strategy: "database"
  // adapter: PrismaAdapter(prisma), // Ví dụ với Prisma

  // Cấu hình trang đăng nhập, đăng xuất, lỗi tuỳ chỉnh
  pages: {
    // signIn: '/auth/signin', // Ví dụ trỏ đến trang đăng nhập tuỳ chỉnh
    // signOut: '/auth/signout',
    // error: '/auth/error', // Trang hiển thị lỗi
  },

  // Cấu hình callbacks (jwt, session, signIn, redirect,...)
  // callbacks: {
  //   async jwt({ token, user }) {
  //     // Thêm thông tin user vào token
  //     return token;
  //   },
  //   async session({ session, token }) {
  //     // Thêm thông tin từ token vào session object
  //     return session;
  //   },
  // },

  // Thêm secret key
  secret: process.env.NEXTAUTH_SECRET,

  // Cấu hình debugging (chỉ bật trong môi trường dev)
  // debug: true,
});

Giải thích code:

  • import NextAuth from "next-auth";: Import hàm chính từ thư viện NextAuth.js.
  • export default NextAuth({ ... });: Export cấu hình NextAuth.js. Hàm NextAuth nhận một đối tượng cấu hình.
  • providers: [...]: Đây là mảng chứa cấu hình cho tất cả các phương thức đăng nhập bạn muốn hỗ trợ (ví dụ: Google, GitHub, Email, đăng nhập truyền thống với username/password).
  • session: { strategy: "jwt" }: Cấu hình cách NextAuth.js quản lý phiên đăng nhập. 'jwt' (JSON Web Token) là mặc định và hoạt động tốt với Serverless functions vì nó không cần server-side state. 'database' cần một Adapter để lưu trữ phiên trong DB.
  • pages: { ... }: Cho phép bạn chỉ định các trang tuỳ chỉnh cho luồng xác thực (đăng nhập, đăng xuất, lỗi...). Nếu không cấu hình, NextAuth.js sẽ dùng các trang mặc định của nó.
  • callbacks: { ... }: Đây là nơi bạn có thể can thiệp vào các luồng xử lý của NextAuth.js, ví dụ như thêm thông tin tuỳ chỉnh vào token JWT hoặc session object sau khi đăng nhập.
  • secret: process.env.NEXTAUTH_SECRET: Cực kỳ quan trọng! Bạn phải cung cấp một secret key ngẫu nhiên để NextAuth.js ký (sign) và mã hóa (encrypt) các token và cookie. Key này phải được lưu trữ an toàn, tốt nhất là trong biến môi trường (.env.local).
  • debug: true: Tùy chọn hữu ích để xem log chi tiết của NextAuth.js trong quá trình phát triển.
Bước 3: Thêm Biến Môi Trường

Như đã đề cập, bạn cần một NEXTAUTH_SECRET. Nếu sử dụng các Providers OAuth (như Google, GitHub), bạn cũng cần CLIENT_IDCLIENT_SECRET của chúng. Tạo một file .env.local ở thư mục gốc của dự án (nếu chưa có).

Bạn có thể tạo một secret key ngẫu nhiên bằng cách chạy lệnh sau trong terminal:

openssl rand -base64 32
# hoặc
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Sao chép chuỗi base64 được tạo ra và dán vào file .env.local:

# .env.local

NEXTAUTH_SECRET=mot_chuoi_ngau_nhien_rat_dai_va_bao_mat_cua_ban_o_day

# Ví dụ cấu hình Google Provider
GOOGLE_ID=YOUR_GOOGLE_CLIENT_ID
GOOGLE_SECRET=YOUR_GOOGLE_CLIENT_SECRET

# Ví dụ cấu hình GitHub Provider
GITHUB_ID=YOUR_GITHUB_CLIENT_ID
GITHUB_SECRET=YOUR_GITHUB_CLIENT_SECRET

# Ví dụ cấu hình Database Adapter (nếu dùng)
# DATABASE_URL=mongodb://localhost:27017/mydb

Lưu ý: Đảm bảo file .env.local được thêm vào .gitignore để tránh push lên public repository.

Bước 4: Thêm Các Providers Xác Thực

Bây giờ, chúng ta sẽ thêm các phương thức đăng nhập vào mảng providers trong file pages/api/auth/[...nextauth].js. NextAuth.js hỗ trợ rất nhiều Providers có sẵn.

Ví dụ 1: Thêm Google Provider (OAuth)

Bạn cần đăng ký ứng dụng của mình với Google Developer Console để nhận CLIENT_IDCLIENT_SECRET. Thêm URI Redirect cho Google Provider là [YOUR_APP_URL]/api/auth/callback/google.

// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google"; // <--- Import Provider

export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID, // <--- Lấy từ biến môi trường
      clientSecret: process.env.GOOGLE_SECRET, // <--- Lấy từ biến môi trường
    }),
    // Thêm các Providers khác ở đây
  ],
  secret: process.env.NEXTAUTH_SECRET,
  session: { strategy: "jwt" },
});

Giải thích code:

  • import GoogleProvider from "next-auth/providers/google";: Import provider cụ thể cho Google.
  • GoogleProvider({ ... }): Khởi tạo Google Provider, truyền vào clientIdclientSecret lấy từ biến môi trường.

Ví dụ 2: Thêm Credentials Provider (Đăng nhập truyền thống)

Credentials Provider cho phép bạn xử lý logic đăng nhập bằng username/email và password của riêng bạn. Đây là nơi bạn kết nối với cơ sở dữ liệu của mình để kiểm tra thông tin người dùng.

// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials"; // <--- Import Provider
// Giả định bạn có hàm verifyPassword để kiểm tra mật khẩu
// import { verifyPassword } from '../../../lib/auth';
// Giả định bạn có hàm findUserByEmail để tìm user trong DB
// import { findUserByEmail } from '../../../lib/db';


export default NextAuth({
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
    CredentialsProvider({ // <--- Cấu hình Credentials Provider
      // Tên provider hiển thị trên form đăng nhập (nếu dùng trang mặc định)
      name: "Credentials",
      // Các trường thông tin bạn muốn yêu cầu người dùng nhập
      credentials: {
        email: { label: "Email", type: "text", placeholder: "jsmith@example.com" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials, req) {
        // <--- Đây là nơi xử lý logic xác thực của bạn
        // 1. Tìm người dùng trong database dựa trên credentials.email
        // const user = await findUserByEmail(credentials.email);

        // if (!user) {
          // Nếu không tìm thấy user, trả về null hoặc ném lỗi
        //   throw new Error('No user found with the email');
        // }

        // 2. So sánh mật khẩu đã hash trong database với mật khẩu người dùng nhập
        // const isValid = await verifyPassword(credentials.password, user.hashedPassword);

        // if (!isValid) {
          // Nếu mật khẩu không khớp
        //   throw new Error('Could not log you in');
        // }

        // 3. Nếu xác thực thành công, trả về đối tượng user.
        //    Thông tin trong đối tượng user này sẽ được đưa vào JWT token/session.
        // return user; // <--- Trả về user object

        // Ví dụ đơn giản: Giả lập xác thực thành công
        console.log('Credentials received:', credentials);
        if (credentials.email === 'test@example.com' && credentials.password === 'password') {
            const user = { id: 1, name: 'J Smith', email: 'test@example.com' };
            return user; // Trả về đối tượng user
        } else {
            // Nếu xác thực thất bại
            return null; // Trả về null để báo hiệu đăng nhập không thành công
        }
      }
    })
  ],
  secret: process.env.NEXTAUTH_SECRET,
  session: { strategy: "jwt" },
});

Giải thích code:

  • import CredentialsProvider from "next-auth/providers/credentials";: Import provider cho Credentials.
  • CredentialsProvider({ ... }): Cấu hình provider này.
  • name: "Credentials": Tên hiển thị cho phương thức này.
  • credentials: { ... }: Định nghĩa các trường input mà provider này cần (ví dụ: email, password). labeltype giúp NextAuth.js tạo form mặc định nếu bạn dùng trang đăng nhập của nó.
  • async authorize(credentials, req) { ... }: Đây là hàm quan trọng nhất của Credentials Provider. Nó nhận đầu vào là credentials (đối tượng chứa giá trị từ form nhập liệu) và req (request object). Nhiệm vụ của bạn là thực hiện logic xác thực tại đây: tìm người dùng trong database, so sánh mật khẩu.
    • Nếu xác thực thành công, trả về một đối tượng user. Đối tượng này ít nhất phải có một trường định danh duy nhất (như id). Các thông tin khác (name, email, image...) cũng nên được thêm vào để sử dụng sau này trong session.
    • Nếu xác thực thất bại, trả về null hoặc ném lỗi (throw new Error(...)). NextAuth.js sẽ xử lý phần còn lại (chuyển hướng đến trang lỗi, v.v.).
Bước 5: Cấu Hình Session Provider Ở Root App

Để có thể truy cập trạng thái xác thực (session) ở bất kỳ đâu trong ứng dụng bằng React Context, bạn cần wrap root component của mình (thường là trong _app.js cho Pages Router hoặc root layout.js cho App Router) bằng <SessionProvider>.

Đối với Pages Router (pages/_app.js):

// pages/_app.js
import { SessionProvider } from "next-auth/react";
import '../styles/globals.css'; // Import global styles của bạn

function MyApp({ Component, pageProps }) {
  return (
    // Wrap ứng dụng của bạn bằng SessionProvider
    <SessionProvider session={pageProps.session}>
      <Component {...pageProps} />
    </SessionProvider>
  );
}

export default MyApp;

Giải thích code:

  • import { SessionProvider } from "next-auth/react";: Import component SessionProvider.
  • <SessionProvider session={pageProps.session}>: Component này sử dụng React Context để cung cấp session object cho toàn bộ cây component con. pageProps.session được lấy từ getServerSideProps nếu bạn sử dụng nó để pre-fetch session.

Đối với App Router (ví dụ: app/layout.tsx hoặc một component wrapper client):

Trong App Router, bạn sẽ cần tạo một component Client Component để wrap Layout hoặc Page của mình:

// app/providers.tsx (Tạo file mới)
'use client'; // <--- Đây là Client Component

import { SessionProvider } from "next-auth/react";

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

Sau đó sử dụng nó trong root layout:

// app/layout.tsx
import './globals.css';
import Providers from './providers'; // <--- Import wrapper

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Providers> {/* <--- Sử dụng wrapper */}
          {children}
        </Providers>
      </body>
    </html>
  );
}

Giải thích code (App Router):

  • 'use client';: Bắt buộc phải có vì <SessionProvider> sử dụng Context API của React, vốn chỉ chạy được ở client-side.
  • import { SessionProvider } from "next-auth/react";: Import component.
  • <SessionProvider>{children}</SessionProvider>: Wrap nội dung ứng dụng để session context khả dụng. Trong App Router, bạn không cần truyền prop session từ pageProps như Pages Router nếu chỉ dùng client-side hook useSession.
Bước 6: Sử Dụng Hook useSession

Đây là cách phổ biến nhất để truy cập trạng thái session trong các React Components (đặc biệt là Client Components). Hook useSession trả về một đối tượng chứa trạng thái session và các hàm tiện ích.

// components/AuthStatus.js
import { useSession, signIn, signOut } from "next-auth/react";

function AuthStatus() {
  // useSession hook trả về trạng thái session và data
  const { data: session, status } = useSession();

  // Trạng thái có thể là "loading", "authenticated", "unauthenticated"

  if (status === "loading") {
    return <p>Đang tải session...</p>;
  }

  if (status === "authenticated") {
    return (
      <div>
        <p>Chào mừng, **{session.user.name || session.user.email}**!</p>
        <button onClick={() => signOut()}>Đăng xuất</button> {/* <--- Sử dụng signOut */}
      </div>
    );
  }

  // status === "unauthenticated"
  return (
    <div>
      <p>Bạn chưa đăng nhập.</p>
      <button onClick={() => signIn()}>Đăng nhập</button> {/* <--- Sử dụng signIn */}
      {/* Hoặc có thể gọi signIn('google') để đăng nhập bằng Google */}
      {/* <button onClick={() => signIn('google')}>Đăng nhập với Google</button> */}
    </div>
  );
}

export default AuthStatus;

Giải thích code:

  • import { useSession, signIn, signOut } from "next-auth/react";: Import hook useSession và các hàm signIn, signOut từ package client của NextAuth.js.
  • const { data: session, status } = useSession();: Gọi hook. Nó sẽ tự động subscribe vào Context được cung cấp bởi <SessionProvider> và trả về trạng thái status (loading, authenticated, unauthenticated) và dữ liệu data (chính là session object hoặc null).
  • status === "loading": NextAuth.js đang kiểm tra session ban đầu hoặc đang trong quá trình đăng nhập/xuất.
  • status === "authenticated": Người dùng đã đăng nhập thành công. session.user chứa thông tin về người dùng.
  • status === "unauthenticated": Người dùng chưa đăng nhập hoặc đã đăng xuất.
  • onClick={() => signOut()}: Hàm signOut được gọi khi nút Đăng xuất được click. Nó gửi yêu cầu đến API route của NextAuth.js để kết thúc session.
  • onClick={() => signIn()}: Hàm signIn được gọi khi nút Đăng nhập được click. Nếu không truyền tham số provider, nó sẽ chuyển hướng người dùng đến trang đăng nhập mặc định của NextAuth.js (nếu bạn có nhiều provider). Bạn có thể truyền tên provider (signIn('google')) để bỏ qua trang chọn provider và chuyển hướng trực tiếp đến trang đăng nhập của Google.
Bước 7: Thực Hiện Đăng Nhập và Đăng Xuất

Như đã thấy ở ví dụ useSession, các hàm signInsignOut từ next-auth/react là cách đơn giản nhất để kích hoạt luồng xác thực từ phía client.

Đăng nhập:

Bạn có thể gọi signIn() trong một event handler.

// Ví dụ: Nút đăng nhập Google
<button onClick={() => signIn('google')}>
  Đăng nhập với Google
</button>

// Ví dụ: Nút đăng nhập bằng Credentials
// Giả định bạn có form nhập email/password và state để lưu giá trị
// async function handleCredentialSignIn(event) {
//   event.preventDefault();
//   const result = await signIn('credentials', {
//     redirect: false, // Không chuyển hướng tự động, xử lý bằng code
//     email: emailState,
//     password: passwordState,
//   });
//   if (result.error) {
//     // Xử lý lỗi đăng nhập (ví dụ: hiển thị thông báo)
//     console.error(result.error);
//   } else {
//     // Đăng nhập thành công, có thể chuyển hướng thủ công nếu cần
//     // router.push('/dashboard');
//   }
// }
// <button onClick={handleCredentialSignIn}>Đăng nhập</button>

Giải thích code:

  • signIn(providerId, options): Hàm này nhận tên provider ('google', 'credentials', v.v.) làm tham số đầu tiên và một đối tượng options tùy chọn.
  • redirect: false: Khi sử dụng Credentials Provider và muốn xử lý lỗi hoặc chuyển hướng sau khi xác thực ngay tại client, bạn nên đặt redirect: false. NextAuth.js sẽ không tự động chuyển hướng mà trả về kết quả (bao gồm lỗi nếu có).
  • Các tham số khác trong options (như email, password cho credentials) sẽ được truyền đến hàm authorize của Credentials Provider.

Đăng xuất:

<button onClick={() => signOut()}>
  Đăng xuất
</button>

// Hoặc với tuỳ chọn redirect sau khi đăng xuất
// <button onClick={() => signOut({ callbackUrl: '/' })}>
//   Đăng xuất và về trang chủ
// </button>

Giải thích code:

  • signOut(options): Hàm này nhận một đối tượng options tùy chọn.
  • callbackUrl: Chỉ định URL mà người dùng sẽ được chuyển hướng đến sau khi đăng xuất thành công. Mặc định là trang hiện tại.
Bước 8: Bảo Vệ Các Trang (Protecting Pages)

Bạn chắc chắn muốn một số trang chỉ dành cho người dùng đã đăng nhập. Có nhiều cách để làm điều này trong Next.js với NextAuth.js.

Cách 1: Sử dụng useSession và Redirect ở Client-side (Phù hợp với Client Components/CSR)

Đây là cách phổ biến khi sử dụng hook useSession. Bạn kiểm tra trạng thái session và chuyển hướng nếu người dùng chưa đăng nhập.

// pages/protected-page.js (Ví dụ Pages Router)
// hoặc app/protected/page.tsx (Ví dụ App Router - với 'use client')
'use client'; // Nếu dùng App Router

import { useSession } from "next-auth/react";
import { useRouter } from "next/router"; // hoặc "next/navigation" cho App Router
import { useEffect } from "react";

function ProtectedPage() {
  const { data: session, status } = useSession();
  const router = useRouter();

  useEffect(() => {
    // Chỉ chạy logic này khi session status không còn là 'loading'
    if (status !== "loading" && status === "unauthenticated") {
      // Nếu chưa đăng nhập, chuyển hướng về trang đăng nhập
      router.push("/auth/signin"); // Hoặc trang nào bạn muốn
    }
  }, [status, router]); // Dependencies: chạy lại khi status hoặc router thay đổi

  // Hiển thị loading hoặc nội dung chỉ khi đã xác thực
  if (status === "loading") {
    return <p>Đang kiểm tra xác thực...</p>;
  }

  if (status === "authenticated") {
    return (
      <div>
        <h1>Trang Bảo Vệ</h1>
        <p>Chào mừng, **{session.user.name}**! Bạn đã đăng nhập thành công.</p>
        {/* Nội dung trang chỉ dành cho người dùng đã đăng nhập */}
      </div>
    );
  }

  // Nếu status là 'unauthenticated', component sẽ không hiển thị nội dung này
  // vì đã redirect trong useEffect.
  return null; // hoặc một fallback UI khác
}

// Đối với Pages Router, có thể thêm getServerSideProps để pre-fetch session ban đầu nhanh hơn
// export async function getServerSideProps(context) {
//   const session = await getSession(context); // <--- Sử dụng getSession ở server-side
//
//   if (!session) {
//     return {
//       redirect: {
//         destination: '/auth/signin', // Trang đăng nhập
//         permanent: false,
//       },
//     };
//   }
//
//   return {
//     props: { session }, // Truyền session xuống component thông qua props
//   };
// }


export default ProtectedPage;

Giải thích code:

  • useSession(): Lấy trạng thái session.
  • useEffect(() => { ... }, [status, router]);: Hook này chạy sau khi component render. Chúng ta kiểm tra status trong effect.
  • if (status !== "loading" && status === "unauthenticated"): Điều kiện kiểm tra: Nếu đã load xong trạng thái session trạng thái là chưa xác thực.
  • router.push("/auth/signin"): Sử dụng Next.js Router để chuyển hướng người dùng đến trang đăng nhập.

Cách 2: Sử dụng getSession ở Server-side (Phù hợp với Pages Router getServerSideProps/getInitialProps)

Cách này giúp bạn kiểm tra session trước khi trang được render ở phía server, ngăn chặn việc hiển thị nội dung nhạy cảm dù chỉ trong giây lát và xử lý redirect nhanh hơn.

// pages/protected-ssr.js (Chỉ dùng cho Pages Router)
import { getSession } from "next-auth/react";

function ProtectedSSRPage({ session }) { // Nhận session từ getServerSideProps
  // Ở đây, bạn chắc chắn rằng session đã tồn tại
  return (
    <div>
      <h1>Trang Bảo Vệ (SSR)</h1>
      <p>Chào mừng, **{session.user.name}**!</p>
      {/* Nội dung trang chỉ dành cho người dùng đã đăng nhập */}
    </div>
  );
}

export async function getServerSideProps(context) {
  // Lấy session ở phía server
  const session = await getSession(context);

  // Kiểm tra nếu không có session
  if (!session) {
    // Chuyển hướng người dùng đến trang đăng nhập
    return {
      redirect: {
        destination: '/auth/signin', // URL trang đăng nhập của bạn
        permanent: false, // false: redirect tạm thời (302), true: redirect vĩnh viễn (301)
      },
    };
  }

  // Nếu có session, truyền nó xuống component qua props
  return {
    props: { session },
  };
}

export default ProtectedSSRPage;

Giải thích code:

  • import { getSession } from "next-auth/react";: Import hàm getSession. Lưu ý rằng mặc dù import từ next-auth/react, hàm này được thiết kế để chạy cả ở server và client.
  • async function getServerSideProps(context) { ... }: Hàm này chạy ở phía server trên mỗi request.
  • const session = await getSession(context);: Gọi hàm getSession, truyền vào context của request (chứa cookie, headers, v.v.). NextAuth.js sẽ dùng thông tin này để kiểm tra session cookie và xác thực.
  • if (!session): Nếu getSession trả về null hoặc undefined, nghĩa là người dùng chưa đăng nhập.
  • return { redirect: { ... } };: Trả về một đối tượng redirect để yêu cầu Next.js thực hiện chuyển hướng phía server.

Cách 3: Sử dụng Middleware (Đối với App Router)

Trong App Router, Middleware là cách mạnh mẽ để bảo vệ các routes dựa trên session trước khi request đến Pages hoặc Route Handlers.

Tạo file middleware.ts (hoặc .js) ở thư mục gốc của dự án.

// middleware.ts
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";

export default withAuth(
  // Hàm callback chạy sau khi auth middleware hoàn thành
  function middleware(req) {
    // console.log("middleware", req.nextUrl.pathname, req.nextauth.token);
    // Ví dụ: Chỉ cho phép admin truy cập '/admin'
    // if (req.nextUrl.pathname.startsWith('/admin') && req.nextauth.token?.role !== 'admin') {
    //   return NextResponse.redirect(new URL('/auth/signin', req.url));
    // }
  },
  {
    // Cấu hình NextAuth.js cho middleware
    callbacks: {
      // authorization callback: Xác định người dùng có được phép tiếp tục không.
      // Trả về TRUE nếu được phép, FALSE nếu không được phép (sẽ redirect đến signIn page),
      // hoặc một URL để redirect đến (nếu muốn chuyển hướng đến trang khác ngoài signIn).
      authorized: ({ token, req }) => {
        // token là JWT token nếu session strategy là 'jwt'
        // req là request object
        const pathname = req.nextUrl.pathname;

        // Ví dụ: Chỉ cho phép người dùng đã xác thực truy cập '/protected' và '/admin'
        if (pathname.startsWith('/protected')) {
           return !!token; // Cho phép nếu token tồn tại (đã đăng nhập)
        }
        // Ví dụ: Cho phép mọi người truy cập các route khác
        return true;
      },
    },
    // Cấu hình trang đăng nhập cho middleware
    pages: {
      signIn: '/auth/signin', // URL trang đăng nhập tuỳ chỉnh của bạn
    },
  }
);

// Định nghĩa các route mà middleware sẽ áp dụng
// export const config = {
//   matcher: ['/protected/:path*', '/admin/:path*'], // Áp dụng cho mọi path dưới /protected và /admin
// };

Giải thích code:

  • import { withAuth } from "next-auth/middleware";: Import wrapper withAuth.
  • export default withAuth(...): Wrap cấu hình middleware của bạn bằng withAuth. withAuth tự động kiểm tra session.
  • Hàm middleware bên trong (function middleware(req) { ... }): Hàm này chạy sau khi withAuth đã kiểm tra session và thêm thông tin auth vào req.nextauth. Bạn có thể sử dụng req.nextauth.token (nếu dùng JWT) hoặc req.nextauth.session (nếu dùng database session và cấu hình adapter) để kiểm tra chi tiết hơn (ví dụ: vai trò người dùng) và thực hiện chuyển hướng tùy ý bằng NextResponse.redirect.
  • callbacks: { authorized: ({ token, req }) => { ... } }: Callback authorized là nơi chính để bạn định nghĩa logic cho phép truy cập route.
    • Nó nhận token (hoặc session nếu dùng database) và req.
    • Bạn trả về true nếu người dùng được phép truy cập route hiện tại.
    • Trả về false nếu không được phép. withAuth sẽ tự động chuyển hướng người dùng đến trang signIn được cấu hình trong pages.
    • Bạn cũng có thể trả về một URL object để chuyển hướng đến một trang khác ngoài trang signIn.
  • pages: { signIn: '/auth/signin' }: Cấu hình này cho withAuth biết trang đăng nhập của bạn ở đâu khi cần redirect.
  • export const config = { matcher: [...] };: (Optional nhưng nên dùng) Cấu hình matcher để chỉ định rõ middleware sẽ chỉ chạy trên những đường dẫn nào. Điều này giúp tối ưu hiệu suất.
Bước 9: Thêm Database Adapter (Tuỳ chọn)

Nếu bạn muốn lưu trữ thông tin người dùng và phiên đăng nhập trong database thay vì chỉ dùng JWT (ví dụ: để quản lý tài khoản người dùng, liên kết nhiều Provider với một user duy nhất), bạn cần cấu hình Database Adapter.

NextAuth.js hỗ trợ nhiều Adapter phổ biến (Prisma, Mongoose, TypeORM, MikroORM, Sequelize, Drizzle, ...). Bạn cần cài đặt adapter tương ứng và thư viện ORM/Client của nó.

npm install @next-auth/prisma-adapter prisma @prisma/client
# hoặc với Mongoose
# npm install @next-auth/mongoose-adapter mongoose

Sau khi cài đặt và thiết lập ORM/Client (ví dụ: cấu hình Prisma schema, chạy migration), bạn thêm Adapter vào cấu hình NextAuth.js:

// pages/api/auth/[...nextauth].js
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";

// Import Adapter và client DB của bạn
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient(); // Khởi tạo client DB

export default NextAuth({
  providers: [
    // ... các providers của bạn
    GoogleProvider({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
    CredentialsProvider({
       // ... cấu hình credentials provider
       // **Quan trọng:** Khi dùng Database Adapter, hàm authorize của Credentials
       // chỉ cần trả về user object. NextAuth.js sẽ tự động lưu user này vào DB
       // hoặc liên kết nếu user đã tồn tại.
       async authorize(credentials, req) {
           // ... logic tìm và kiểm tra user trong DB của bạn
           // const user = await findUserUserInYourDB(credentials.email, credentials.password);
           // return user; // Trả về user object từ DB
           // Ví dụ đơn giản
           if (credentials.email === 'test@example.com' && credentials.password === 'password') {
             // Khi dùng adapter, user object trả về cần có ID từ DB
             return { id: "123", name: "Test User", email: "test@example.com" };
           }
           return null;
       }
    })
  ],
  // <--- Thêm Adapter vào cấu hình
  adapter: PrismaAdapter(prisma),

  // Khi dùng adapter, session strategy mặc định là "database"
  // Không cần set session strategy: "jwt" nữa trừ khi bạn có lý do đặc biệt.
  // session: { strategy: "database" }, // Có thể bỏ qua dòng này

  secret: process.env.NEXTAUTH_SECRET,

  // Cấu hình callbacks nếu cần tuỳ chỉnh thông tin user/session
  // callbacks: {
  //   async session({ session, user, token }) {
  //      // Khi dùng database strategy, callback session nhận user object từ DB
  //      // Thay vì token. Bạn có thể thêm thông tin khác từ user object vào session.
  //      session.user.id = user.id; // Ví dụ: Thêm user ID vào session
  //      return session;
  //   }
  // }

  // ... các cấu hình khác
});

Giải thích code:

  • import { PrismaAdapter } from "@next-auth/prisma-adapter";: Import Adapter cho Prisma. Tên import có thể khác với các Adapter khác.
  • import { PrismaClient } from "@prisma/client";: Import client của Prisma.
  • const prisma = new PrismaClient();: Khởi tạo client.
  • adapter: PrismaAdapter(prisma),: Thêm dòng này vào đối tượng cấu hình chính của NextAuth.js. Truyền instance của client DB của bạn vào Adapter. NextAuth.js sẽ tự động sử dụng Adapter này để tạo/cập nhật user, account, session khi người dùng đăng nhập.
  • Credentials authorize khi dùng Adapter: Khi dùng Database Adapter, hàm authorize của Credentials Provider vẫn chịu trách nhiệm tìm và xác thực user dựa trên thông tin đầu vào. Tuy nhiên, thay vì chỉ trả về một user object đơn giản, bạn nên trả về user object lấy từ database của bạn. NextAuth.js Adapter sẽ nhận user object này và tự động lưu session vào DB, hoặc liên kết với user đã tồn tại nếu người dùng đăng nhập bằng nhiều phương thức khác nhau.
  • Session Callback: Khi sử dụng database session (strategy: "database" - đây là mặc định khi có Adapter), callback session sẽ nhận đối tượng user từ database thay vì token. Đây là nơi bạn có thể gắn thêm các trường từ user object vào session object trước khi nó được gửi đến client.

Comments

There are no comments at the moment.