Nếu các bạn chưa xem phần 1 thì đây là danh sách các phần của bài viết này

Ở phần 1, chúng ta đã kết thúc tại bước đăng nhập sử dụng Firebase Authentication. Đến đây, nếu bạn thấy như vậy đã đủ thì bạn có thể tự tùy chỉnh ứng dụng của mình và không cần xem tiếp phần 2 nữa

Còn nếu bạn đang tìm kiếm những chức năng nâng cao hơn thì hãy cùng tôi đi hết bài viết này (tham khảo các chức năng chỉ Firebase Admin cung cấp):

  • Manage Users
  • Import Users
  • Create Custom Tokens
  • Verify ID Tokens
  • Manage User Sessions
  • Manage Session Cookies => chúng ta sẽ sử dụng tính năng này
  • Control Access with Custom Claims
  • Generating Email Action Links

Sử dụng Firebase Admin SDK

Cách đơn giản nhất là các bạn truy cập vào Firebase Console và tạo private key

Tạo private key
Tạo private key (Xem ảnh gốc)

Thông tin serviceAccount được download về sẽ có dạng như sau

// test12312-7c498-firebase-adminsdk-g5lvb.json
{
  "type": "...",
  "project_id": "<YOUR_PROJECT_ID>",   
  "private_key_id": "...",
  "private_key": "<YOUR_PRIVATE_KEY>",  
  "client_email": "<YOUR_CLIENT_EMAIL>",
  "client_id": "...",
  "auth_uri": "...",
  "token_uri": "...",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "..."
}

Chúng ta cần phải cài đặt firebase-admincookie

$ yarn add firebase-admin
$ yarn add cookie

Tạo file firebase-admin.ts rồi thêm cài đặt đã lấy được ở bước trên như sau

// ./libs/firebase-admin.ts

import { cert, initializeApp, getApps } from 'firebase-admin/app'

const serviceAccount = {
  projectId: 'YOUR_PROJECT_ID',
  clientEmail: '<YOUR_CLIENT_EMAIL>',
  privateKey: `<YOUR_PRIVATE_KEY>`,
}

if (!getApps().length) {
  initializeApp({
    credential: cert(serviceAccount)
  })
}

Tạo session cookie thông qua api

Đến thời điểm hiện tại, thì chúng ta mới chỉ đang cài đặt để có thể sử dụng Firebase Admin ở phía backend. Ở bước này chúng ta sẽ tạo session cookie sau khi user đã đăng nhập thành công thông qua Firebase Authentication

Tạo mới file api/create-session.ts

// ./pages/api/create-session.ts

import type { NextApiRequest, NextApiResponse } from "next";
import { serialize } from "cookie";

import "../../libs/firebase-admin";
import { getAuth } from "firebase-admin/auth";

type Data = {
  error: boolean;
};

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  const { idToken } = req.body;
  const expiresIn = 60 * 60 * 24 * 5 * 1000;

  getAuth()
    .createSessionCookie(idToken, { expiresIn })
    .then(
      (sessionCookie) => {
        const options = {
          path: "/",
          maxAge: expiresIn,
          httpOnly: true,
          secure: true,
        };

        res.setHeader(
          "Set-Cookie",
          serialize("session", sessionCookie, options)
        );

        res.status(200).json({ error: false });
      },
      (error) => {
        console.log("error", error);
        res.status(401).json({ error: true });
      }
    );
}

Phía frontend, chúng ta cần gọi api vừa được tạo ra

// ./services/sign.ts

// ...
export const signInWithEmail = (email: string, password: string) => {
  return signInWithEmailAndPassword(auth, email, password)
    .then((userCredential) => {
      console.log(userCredential.user);
      const user = userCredential.user;

      user.getIdToken().then((idToken) => {
        console.log("idToken", idToken);

        // Gọi tới /api/create-session
        fetch("/api/create-session", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },

          body: JSON.stringify({ idToken }),
        }).then((res) => {
          console.log("res", res);
          window.location.href = "/protected";

        });
      });
    })
    .catch((error) => {
      console.log(error.code);
    });
};

Kết quả nên được trả về từ api như sau. Chú ý dòng Set-Cookie, do chúng ta đã tạo ra session cookie tại /api/create-session

session đã được tạo ra
session đã được tạo ra (Xem ảnh gốc)

Xác thực session cookie

Cài đặt cơ chế xác thực session cookie sử dụng getAuth().verifySessionCookie

// ./middlewares/authentication.ts

import '../libs/firebase-admin'
import { getAuth } from 'firebase-admin/auth'
import { GetServerSideProps } from 'next'

type GetServerHandlerParam = Parameters<GetServerSideProps>
type GetServerHandlerReturnType = ReturnType<GetServerSideProps>

export const _authentication = (
  callback: (context: GetServerHandlerParam[0]) => GetServerHandlerReturnType
) => {
  const getServerSideProps: GetServerSideProps = async (context) => {
    const { req } = context
    const url = req.url || ''
    const session = req.cookies['session']

    try {
      const decoded = await getAuth().verifySessionCookie(session, true)
      console.log('decoded', decoded)
      if (url.includes('/login')) {
        return {
          redirect: {
            destination: '/',
            permanent: false,
          },
        }
      }
    } catch (error) {
      if (url.includes('/protected')) {
        console.log('=>>', 'JWT session is invalid')
        return {
          redirect: {
            destination: '/login',
            permanent: false,
          },
        }
      }
    }

    const returnData = callback(context)

    return returnData
  }

  return getServerSideProps
}

Tạo trang protected để kiểm tra việc hoạt động

// ./pages/protected.tsx

import { GetServerSideProps } from "next";
import { _authentication } from "../middlewares/authentication";

export default function ProtectedPage() {
  return (
    <div className="h-screen">
      <div className="bg-white min-h-full px-4 py-16 sm:px-6 sm:py-24 md:grid md:place-items-center lg:px-8">
        <div className="max-w-max mx-auto">
          <main className="sm:flex">
            <p className="text-4xl font-extrabold text-indigo-600 sm:text-5xl">
              200
            </p>
            <div className="sm:ml-6">
              <div className="sm:border-l sm:border-gray-200 sm:pl-6">
                <h1 className="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">
                  Protected page
                </h1>
                <p className="mt-1 text-base text-gray-500">
                  Only verified user can access to this page
                </p>
              </div>
              <div className="mt-10 flex space-x-3 sm:border-l sm:border-transparent sm:pl-6">
                <a
                  href="/"
                  className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                  Go back home
                </a>
                <a
                  href="#"
                  className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
                >
                  Log out
                </a>
              </div>
            </div>
          </main>
        </div>
      </div>
    </div>
  );
}

export const getServerSideProps: GetServerSideProps = _authentication(
  async () => {
    return {
      props: {
        data: [1],
      },
    };
  }
);

Cập nhật lại trang chủ, cho phép truy cập vào trang /protected

// ./pages/index.tsx

import type { NextPage } from "next";

const Home: NextPage = () => {
  return (
    <div className="m-6">
      // ......

      <a
        href="/protected"
        className="ml-4 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
      >
        To protected page
      </a>
    </div>
  );
};

export default Home;

Thành quả cuối cùng sẽ như sau

Xác thực thành công
Xác thực thành công (Xem ảnh gốc)

Kết luận

Cuối cùng chúng ta cũng cùng nhau đã hoàn thành chức năng xác thực user với Next.js sử dụng Firebase Admin. Hi vọng các bạn đã hiểu được cơ chế làm việc của Firebase qua bài viết này

Nếu có bất kì thắc mắc nào, hãy để lại feedback và tôi sẽ giải đáp. Hẹn gặp lại ở các bài viết tiếp theo