Next.jsのApp RouterでFirebase Authを使いたい。
その場合に問題になってくるのがRSC(Server Component)での認証をどうするかというところが一番の重要なポイント。
セッションCookieを利用する
RSCは従来のサーバーサイドでのセッションCookieの管理と同じ仕組みでいいのではないかと思う。
さらに公式ドキュメントにはその方法も丁寧に解説されている。
セッションCookieを管理する
ざっくりと内容を説明すると以下になる
- Client用のFirebase SDKでログインをする
- ログイン成功時に取得したidTokenをサーバー側に送る
- サーバー側はFirebase Admin SDKの
createSessionCookieを呼び出してセッションCookieを発行する
- 発行された値を
sessionという名前でCookieに保存する
実装を見ている感じシンプルでいいと思う。
実装はこんな感じ
一部抜粋なんだけど、フロントエンド側でのログイン処理は以下のようになる。signInWithEmailAndPasswordでログインして取得したidTokenをloginWithToken関数でサーバーに渡している。
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を使うのはまだまだ有効だと思う。