Firebase AuthとNext.jsで認証をする

Firebase AuthとNext.jsで認証をする

Next.jsのApp RouterでFirebase Authを使いたい。 その場合に問題になってくるのがRSC(Server Component)での認証をどうするかというところが一番の重要なポイント。

セッションCookieを利用する

RSCは従来のサーバーサイドでのセッションCookieの管理と同じ仕組みでいいのではないかと思う。
さらに公式ドキュメントにはその方法も丁寧に解説されている。

セッションCookieを管理する

ざっくりと内容を説明すると以下になる

  1. Client用のFirebase SDKでログインをする
  2. ログイン成功時に取得したidTokenをサーバー側に送る
  3. サーバー側はFirebase Admin SDKのcreateSessionCookieを呼び出してセッションCookieを発行する
  4. 発行された値をsessionという名前でCookieに保存する

実装を見ている感じシンプルでいいと思う。

実装はこんな感じ

一部抜粋なんだけど、フロントエンド側でのログイン処理は以下のようになる。signInWithEmailAndPasswordでログインして取得したidTokenloginWithToken関数でサーバーに渡している。

1
2
3
4
5
6
7
8
      const result = await signInWithEmailAndPassword(
        auth,
        credentials.email,
        credentials.password
      );

      const idToken = await result.user.getIdToken();
      await loginWithToken(idToken); // ここでServer Actionを呼び出してサーバー側にトークンを渡す

そしてServer Action側のloginWithToken関数はこんな感じ。 idTokenを受け取ってcreateSessionCookieを呼び出してセッションCookieを発行している。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"use server";

import { adminAuth } from "@/externals/firebase/server";
import { cookies } from "next/headers";

export async function loginWithToken(idToken: string) {
  try {
    const expiresIn = 60 * 60 * 1000; // 1時間(IDトークンと同期)

    // セッションCookieを作成
    const sessionCookie = await adminAuth.createSessionCookie(idToken, {
      expiresIn,
    });

    const cookieStore = await cookies();
    cookieStore.set("session", sessionCookie, {
      maxAge: expiresIn / 1000,
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "lax",
      path: "/",
    });

    return { success: true };
  } catch (error: unknown) {
    console.error("Login error:", error);
    return {
      success: false,
      error: error instanceof Error ? error.message : "ログインに失敗しました",
    };
  }
}

セッションCookieを検証する

認証が必要なServer Componentでは、以下のようにセッションCookieを検証する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { adminAuth } from "@/externals/firebase/server";
import { cookies } from "next/headers";

export async function getCurrentUser() {
  try {
    const cookieStore = await cookies();
    const sessionCookie = cookieStore.get("session")?.value;

    if (!sessionCookie) {
      return null;
    }

    // セッションCookieを検証
    const decodedClaims = await adminAuth.verifySessionCookie(
      sessionCookie,
      true // checkRevoked: trueにしてトークンの無効化をチェック
    );

    return {
      uid: decodedClaims.uid,
      email: decodedClaims.email,
      // 必要に応じて他のクレームも取得
    };
  } catch (error) {
    console.error("Session verification error:", error);
    return null;
  }
}

ログアウト処理

ログアウト時はCookieを削除し、必要に応じてFirebase側でもトークンを無効化する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
"use server";

import { adminAuth } from "@/externals/firebase/server";
import { cookies } from "next/headers";

export async function logout() {
  try {
    const cookieStore = await cookies();
    const sessionCookie = cookieStore.get("session")?.value;

    if (sessionCookie) {
      // セッションCookieを検証してUIDを取得
      const decodedClaims = await adminAuth.verifySessionCookie(sessionCookie);
      
      // ユーザーのリフレッシュトークンを無効化(オプション)
      await adminAuth.revokeRefreshTokens(decodedClaims.uid);
    }

    // Cookieを削除
    cookieStore.delete("session");

    return { success: true };
  } catch (error) {
    console.error("Logout error:", error);
    // Cookieは削除しておく
    const cookieStore = await cookies();
    cookieStore.delete("session");
    
    return { success: true }; // ログアウトは基本的に成功扱い
  }
}

最後に

最近だとNext.jsで使う認証サービスはSupabase AuthやClerkだったりAuth.jsなど様々な認証のためのソリューションが出てきているが、ちょっとした認証はFirebase Authを使うのはまだまだ有効だと思う。

カテゴリ

comments powered by Disqus