Open7

Next.js(app router) でFirebase Authentication + Next authを使用してログイン機能を実装する。

oo

Q:firebaseだけでログイン機能を作るのではなく、next-authを併用するのはなぜですか?

A:Firebase Authenticationは主にクライアントサイドでのみ動作するため。
NextAuthでサーバーサイドでのセッションを管理する。

FirebaseのクライアントSDKは、ブラウザ上で動作し、ユーザーの認証情報を保持しますが、サーバーサイドではこれを直接参照することができません。そのため、サーバーサイドでユーザーの認証情報を確認する場合、クライアントサイドからトークンを送信し、そのトークンをサーバーサイドで検証する必要があります。

⇧これが面倒だから

NextAuthは、サーバーサイドでのセッションを管理し、APIルートやサーバーコンポーネントで簡単に認証情報を参照できます。これにより、サーバーサイドで認証が必要な操作を行う際に、シームレスな体験が提供されます。

NextAuthをサーバーサイドで使用する。

参考記事

https://zenn.dev/kazukazu3/articles/fe07cc72647368
https://zenn.dev/tentel/articles/cc76611f4010c9


はじめに

https://firebase.google.com/codelabs/firebase-nextjs?hl=ja
これはfirebase公式のNext.js(app router)用のドキュメント。
これを大いに参考にする。

関連レポジトリ

https://github.com/firebase/friendlyeats-web/tree/master/nextjs-start
https://github.com/firebase/friendlyeats-web/tree/master/nextjs-end

oo
  • Firebase CLIをインストールと言っているが今回はインストールしない。
  • エミュレータを使うと言っているが今回は使わない。

ログイン機能とログアウト機能を実装する。

https://firebase.google.com/codelabs/firebase-nextjs?hl=ja#5
上記のページを参照。
src/lib/firebase/配下に機能ごとにファイルを分けて実装するみたい。
今回はそれに従う。(実際過去にnuxt3で実装した時にログインログアウトのロジックをstoreに書くのかどうか、どこに書けばいいのかわからなかったので、libで管理できるならそれがいい)

クライアントサイドのFirebase設定

src/lib/firebase/client.ts
"use client";

import { initializeApp, getApps } from "firebase/app";
import { firebaseConfig } from "./config";
import { getAuth } from "firebase/auth";
export const firebaseApp = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export const auth = getAuth(firebaseApp);

サーバーサイドのFirebase設定

src/lib/firebase/serverApp.ts
// サーバーサイドでのみ実行されるコードを強制
// https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment
import "server-only";

import { headers } from "next/headers";
import { initializeServerApp } from "firebase/app";

import { firebaseConfig } from "./config";
import { getAuth } from "firebase/auth";

export async function getAuthenticatedAppForUser() {
  throw new Error("not implemented");
}

コンフィグ

src/lib/firebase/config.ts
type FirebaseConfig = {
  apiKey: string;
  authDomain: string;
  projectId: string;
  storageBucket: string;
  messagingSenderId: string;
  appId: string;
};

// 環境変数を読み込み、存在しない場合はエラーをスローする関数
function getEnvVariable(key: string): string {
  const value = process.env[key];
  if (value === undefined) {
    throw new Error(`環境変数 ${key} が設定されていません`);
  }
  return value;
}

const config: FirebaseConfig = {
  apiKey: getEnvVariable("NEXT_PUBLIC_FIREBASE_API_KEY"),
  authDomain: getEnvVariable("NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN"),
  projectId: getEnvVariable("NEXT_PUBLIC_FIREBASE_PROJECT_ID"),
  storageBucket: getEnvVariable("NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET"),
  messagingSenderId: getEnvVariable("NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID"),
  appId: getEnvVariable("NEXT_PUBLIC_FIREBASE_APP_ID"),
};

// デプロイ時に引用符("")を削除してクリーンな文字列にする必要がある
Object.keys(config).forEach((key) => {
  const configKey = key as keyof FirebaseConfig;
  const configValue = config[configKey] + "";
  if (configValue.charAt(0) === '"') {
    config[configKey] = configValue.substring(1, configValue.length - 1);
  }
});

export const firebaseConfig = config;

認証ロジック(ログイン・ログアウト・認証状態検知)

src/lib/firebase/auth.ts
import { GoogleAuthProvider, signInWithPopup, onAuthStateChanged as _onAuthStateChanged, User } from "firebase/auth";
import { auth } from "@/lib/firebase/clientApp";

// 認証状態の変化を監視する関数
// ユーザーがサインインまたはサインアウトしたときにコールバック関数が呼び出される
export function onAuthStateChanged(cb: (user: User | null) => void) {
  return _onAuthStateChanged(auth, cb);
}

// Googleアカウントでサインインする関数
export async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();

  try {
    await signInWithPopup(auth, provider);
  } catch (error) {
    console.error("Googleでのサインイン中にエラーが発生しました", error);
    throw error; // エラーを呼び出し元に伝播
  }
}

// サインアウトする関数
export async function signOut() {
  try {
    return auth.signOut(); // Firebaseのサインアウトメソッドを呼び出す
  } catch (error) {
    console.error("サインアウト中にエラーが発生しました", error); // サインアウトに失敗した場合のエラーハンドリング
    throw error; // エラーを呼び出し元に伝播
  }
}

oo

NextAuth×Firebaseで認証管理 in appRouter ~事前知識編~

Firebaseでのログイン後にidTokenをキーとしてNextAuthで再ログインし、認証管理をNextAuthが発行するサーバーサイドCookieに委ねる。

上記についての自分なりの解釈

ログイン時

  1. クライアントorサーバーコンポーネントからログインイベント発火(ログインボタンなどから)
  2. FirebaseAuthで受け取り、ログイン処理実行。⇨ FirebaseAuthがidToken発行

    idTokenとは、ユーザーが認証されたことを示す一時的なトークン

  3. そのidTokenをNextAuthが受け取り、セッションに格納。

    セッションは通常サーバーサイドで作成され、クライアントサイドにはセッションIDが保存される。

  4. セッションでidTokenを保管しておく。

認証情報が必要な時(ユーザー名変更など)

  1. クライアントorサーバーコンポーネントからイベントが発火
  2. そのイベントを受け取ったNextAuthが、セッションからidTokenを取り出す。
    (もし取り出せなかった場合はログインページにリダイレクトする。)
  3. NextAuthがidTokenをFirebaseに受け渡し、FirebaseがそのidTokenを検証して、自分が発行したものと合致するかを確認する。

    idTokenが改ざんされていないか、期限切れじゃないかなど

  4. FirebaseがidTokenから認証情報を取り出す

    idTokenはユーザーが認証されたことを示す認証情報の一部。idTokenにはUIDヤメールアドレスなどが含まれている。

  5. (認証が成功した場合)クライアントorサーバーコンポーネントは認証情報を受け取り、ビジネスロジックの実行

    ビジネスロジックとは、アプリケーションが実行する主要な機能や処理のこと。例えば、ユーザー名の変更、データベースへのデータ保存、特定の条件に基づいた操作など。

  6. (認証が失敗した場合)ログインページにリダイレクト

ログアウト時

  1. クライアントorサーバーコンポーネントからログアウトイベント発火
    2 .NextAuthがそのイベントを検知し、セッション領域からidTokenを削除する。
    3.FirebaseAuthがログアウト処理を実装する。idTokenが無効化される。

    idTokenが無効化されるとは、そのトークンがもはや有効ではなくなることを意味します。つまり、そのidTokenを使って認証を試みても失敗するようになります。これにより、ユーザーが再び認証を受けるまで、認証された状態を保持することができなくなります。

補足知識

idTokenとは?

idTokenとは

  • idToken(Identity Token)は、ユーザーが認証されたことを示すトークンです。Firebase Authenticationがユーザーを認証すると、このトークンが発行されます。
  • このトークンには、ユーザーの識別子(UID)、メールアドレス、その他の情報が含まれています。
  • JWT(JSON Web Token)形式で、セキュアに情報を伝達するために使用されます。

idTokenが持つ情報

  • ユーザーID(UID): Firebase Authによって発行される一意のユーザー識別子。
  • メールアドレス: 認証されたユーザーのメールアドレス(存在する場合)。
  • 発行者: トークンを発行した認証サーバー。
  • 発行日時: トークンが発行された日時。
  • 有効期限: トークンの有効期限。

idTokenの役割

  • 認証: idTokenを使用することで、サーバーはユーザーが正しく認証されたことを確認できます。
  • 認可: idTokenを使ってユーザーが特定の操作を行う権限があるかを確認できます。
セッションとは

セッションは、ユーザーがウェブサイトやアプリケーションを使用している間、そのユーザーの情報をサーバーサイドで一時的に保存する仕組み。
セッションはサーバー側で管理され、セッションIDだけがクライアントに保存されます。

セッションの特徴

サーバーサイドで管理

セッションはサーバー側で管理されます。ユーザーがログインすると、その情報がサーバー上のセッションに保存されます。

一時的

セッションは通常、一時的な情報を保存します。ユーザーがブラウザを閉じるか、セッションの有効期限が切れると、セッションは終了します。

セッションID

セッションを識別するために、サーバーはユーザーごとに一意のセッションIDを発行し、クライアント側(ユーザーのブラウザ)に保存します。このIDを使ってサーバーはどのセッションがどのユーザーのものかを特定します。

セッションの利点

セキュリティ

認証情報がサーバー側に保存されるため、クライアント側に直接保存されるよりも安全です。

サーバー側の処理

サーバーサイドでの処理が必要な場合に、ユーザーの状態を保持できます。

ローカルストレージとは?

ローカルストレージは、ユーザーのブラウザにデータを保存するための仕組みです。

ローカルストレージの特徴

クライアントサイドで管理

ローカルストレージはクライアント側、つまりユーザーのブラウザにデータを保存します。

永続的

保存されたデータはブラウザを閉じても消えません。ユーザーが手動で削除するか、ブラウザの設定を変更しない限り、データは保持されます。

容量制限

ローカルストレージには容量制限があります(通常は5MB程度)。大量のデータを保存するには不向きです。

ローカルストレージの利点

アクセスの簡便さ

クライアント側でデータに簡単にアクセスできます。例えば、ページをリロードしてもデータが保持されます。

シンプルなAPI

ローカルストレージはシンプルなAPIで操作でき、キーと値のペアでデータを保存します。

セッションとローカルストレージの違い

管理場所

  • セッションはサーバー側で管理され、セッションIDだけがクライアントに保存されます。
  • ローカルストレージはクライアント側でデータ全体が保存されます。

セキュリティ

  • セッションはサーバー側でデータを管理するため、セキュリティが高いです。クライアント側に直接認証情報が保存されないため、悪意あるユーザーから保護されやすいです。
  • ローカルストレージはクライアント側にデータが保存されるため、悪意あるユーザーにアクセスされるリスクがあります。

データの持続性

  • セッションは一時的で、ブラウザを閉じるとセッションが切れることが多いです。
  • ローカルストレージはブラウザを閉じてもデータが保持されます。

認証(Authentication)はユーザーが誰であるかを確認するプロセスですが、認可(Authorization)はそのユーザーが何をする権限があるかを確認するプロセスです。

認証情報が必要な時の例
  • ユーザーのプロフィール管理
    • プロフィール更新
    • パスワード変更
    • アカウント削除
  • データの読み取り・書き込み
    • 個人データの読み取り:自身の個人データ、例えばメッセージなどを確認する場合
    • 個人データの書き込み:例えばメッセージを送信する場合
  • サブスクリプションの管理: サブスクリプションの更新やキャンセルを行う場合。
  • プレミアムコンテンツのアクセス: プレミアムユーザー専用のコンテンツにアクセスする場合。
  • 管理者専用ページ: 管理者権限が必要なページ(例えば、ダッシュボードや管理パネル)にアクセスする場合。
  • サードパーティAPIの利用: サードパーティのサービス(例えば、Google APIやFacebook API)を使用する場合、認証情報を必要とすることが多いです。
認可が必要な場合の例

認可が必要な場合の例

管理者機能

  • 管理者ダッシュボードへのアクセス: 一般ユーザーと管理者ユーザーではアクセスできるページが異なります。管理者専用のダッシュボードや設定ページにアクセスする際には、管理者権限が必要です。
  • ユーザー管理: 他のユーザーの情報を閲覧・編集・削除する操作には、特別な権限が必要です。

データのアクセス

  • 個人データへのアクセス: ユーザーは自分のデータにのみアクセスできるべきです。例えば、他のユーザーの個人情報や注文履歴にアクセスするには特別な権限が必要です。

操作の制限

  • 編集権限: 一部のユーザーにのみデータの編集を許可し、他のユーザーには閲覧のみを許可する。
  • 削除権限: データの削除操作は通常リスクが高いため、特定の権限を持つユーザーにのみ許可されます。

サービスの利用制限

  • プレミアム機能の利用: 有料ユーザーだけが利用できる機能やサービスを提供する場合。
  • APIの使用制限: 特定のAPIエンドポイントへのアクセスは、特定の権限を持つユーザーにのみ許可する。

AIによる例え

映画館の入場管理だったら

初心者向けに、Firebase AuthとNextAuthを使った認証の流れをもっと分かりやすく説明します。例えも交えて、各ステップがどのような役割を果たしているのかを解説します。

シナリオ:映画館の入場管理
想像してみてください。あなたは映画館のスタッフです。映画館には入場ゲート(Firebase Auth)と入場券管理システム(NextAuth)があります。これらを使って、映画館に入場するお客さんを管理しています。

  1. お客さんがチケットを見せる(ログインイベント発火)
    お客さんが映画館に来て、入場ゲートにチケットを見せます。これが「ログインイベント」です。

サーバーまたはクライアントコンポーネント
映画館の入り口(アプリのフロントエンド部分)で、お客さんがチケット(ログイン情報)を提示します。
2. 入場ゲートでチケットを確認(Firebase Authによるログイン処理)
入場ゲート(Firebase Auth)がチケットを確認します。チケットが有効であれば、特別なスタンプ(idToken)を発行します。

Firebase Auth
チケットの情報(ログイン情報)を確認し、正しければスタンプ(idToken)を発行します。
3. スタンプをチケット管理システムに記録(NextAuthによるセッション管理)
特別なスタンプ(idToken)は、入場券管理システム(NextAuth)に記録されます。これにより、お客さんが映画館にいる間、どこにいてもスタンプを見せることで本人確認ができます。

NextAuth
スタンプ(idToken)を記録し、映画館内で使用できるようにします。
4. 映画館内のサービス利用(認証情報が必要なイベント発火)
お客さんがポップコーンを買いたいときや、トイレを利用したいときに、スタッフがスタンプ(認証情報)を確認します。

サーバーまたはクライアントコンポーネント
映画館内で特定のサービス(認証が必要な操作)を利用する際に、スタンプ(認証情報)が必要になります。
5. スタンプを確認(idTokenの取り出しと検証)
スタッフが入場券管理システム(NextAuth)にアクセスして、スタンプ(idToken)を確認します。もしスタンプが見つからなければ、入り口に戻って再確認するようにお客さんに伝えます。

NextAuth
スタンプ(idToken)を確認し、正しいことを確かめます。スタンプがない場合は、入り口(ログインページ)に戻るように促します。
6. スタンプが有効ならサービスを提供(認証結果の処理)
スタンプが有効であれば、お客さんにサービスを提供します。無効であれば、再度入り口に戻ってチケットを確認するようにします。

NextAuth
スタンプ(idToken)が有効ならサービスを提供し、無効ならログインページにリダイレクトします。
7. 映画が終わったら帰る(ログアウトイベント発火)
映画が終わったら、お客さんが帰るためにゲートを通ります(ログアウトイベント)。

サーバーまたはクライアントコンポーネント
お客さんが映画館を出るとき(ログアウトイベント)。
8. スタンプを無効化(ログアウト処理)
入場券管理システム(NextAuth)はスタンプを無効化し、入場ゲート(Firebase Auth)はお客さんが帰ったことを記録します。

NextAuth
スタンプ(idToken)を無効化し、入場ゲート(Firebase Auth)もそれを確認します。
図の解説
入場時の流れ
ログインイベント発火: お客さんがチケットを見せる。
Firebase Authによるログイン処理: チケットの確認とスタンプの発行。
NextAuthによるセッション管理: スタンプを記録。
映画館内の利用
認証情報が必要なイベント発火: お客さんがポップコーンを買う。
idTokenの取り出しと検証: スタンプを確認。
認証結果の処理: サービスの提供。
退場時の流れ
ログアウトイベント発火: お客さんが帰る。
ログアウト処理: スタンプの無効化。
このように、Firebase AuthとNextAuthを組み合わせることで、クライアントとサーバーの両方で認証を一貫して管理し、セキュリティを保ちながらユーザーにスムーズな体験を提供できます。

oo

NextAuth×Firebaseで認証管理 in appRouter ~実装編~

https://zenn.dev/tentel/articles/cc76611f4010c9
上記をもとに実装していく。

秘密鍵の生成

  1. firebaseコンソールの「プロジェクトの設定」> 「サービスアカウント」ページの、Firebase Admin SDK 「新しい秘密鍵の生成」で秘密鍵を生成する。
  2. 秘密鍵がPCにダウンロードされる。
  3. めちゃめちゃプロジェクト名なので、firebaseSecretKey.jsonにファイル名を変更
  4. 漏れると大変にまずいので.gitignoreにfirebaseSecretKey.jsonを追加する。
  5. プロジェクトルートに配置。srcと同じ階層。

firebaseの実装

  • src/lib/firebase/client.ts client SDKはログイン認証で利用。
  • src/lib/firebase/admin.ts admin SDKはNextAuthが認証情報を確認する際に、クライアントから送られたidTokenの検証に利用。
src/lib/firebase/admin.ts
mport { initializeApp, cert, getApps } from "firebase-admin/app";
import { getAuth } from "firebase-admin/auth";

const serviceAccount = require("/firebaseSecretKey.json");
export const firebaseAdmin =
  getApps()[0] ??
  initializeApp({
    credential: cert(serviceAccount),
  });

export const auth = getAuth();
SDKとは?

SDKとは「Software Development Kit(ソフトウェア開発キット)」の略。
ソフトウェア開発者が特定のプラットフォームや技術を使用してアプリケーションを開発するためのツールやリソースのセット。

SDKの構成要素

ライブラリとAPI

開発者が特定の機能を実装するために利用できるコードやインターフェースのセットです。例えば、グラフィックス描画、ネットワーク通信、データベースアクセスなどの機能が含まれます。

ドキュメント

SDKの使い方やAPIの説明が書かれたマニュアルやガイドです。これには、チュートリアル、サンプルコード、リファレンスドキュメントなどが含まれます。

ツール

開発プロセスを支援するためのツールやユーティリティが含まれます。例えば、デバッガ、プロファイラ、エミュレータ、ビルドツールなどです。

サンプルコード

SDKの使用方法を示す具体的なコード例です。開発者はこれを参考にして、自分のプロジェクトに適用することができます。

サポートファイル

ヘッダーファイル、リソースファイル、設定ファイルなど、開発をサポートするための追加ファイルが含まれることがあります。

具体例

Firebase SDK

FirebaseはGoogleが提供するクラウドサービスで、Firebase SDKを使用することで、アプリケーションにリアルタイムデータベース、認証、クラウドストレージ、プッシュ通知などの機能を簡単に追加できます。

  • ライブラリとAPI: Firebase Authentication、Firebase Realtime Database、Firebase Cloud Messagingなどのライブラリが含まれます。
  • ドキュメント: 使い方やAPIリファレンスが詳細に書かれたドキュメント。
  • ツール: Firebase CLIなどの開発ツール。
  • サンプルコード: さまざまな機能を実装するためのコード例。

NextAuthをインストール

SSRでも認証情報を参照できるようにするために、NextAuthをインストールします。

npm i next-auth

NextAuthの設定など

NEXTAUTH_SECRETの登録

JWTを暗号化し、トークンをハッシュするために利用するシークレットを登録します。
シークレットの値は何でも良いですが、以下コマンドをターミナル等で叩いて生成される値を利用しましょう。

openssl rand -base64 32
# Next Auth
NEXTAUTH_SECRET=rDo15EMKxXGIkjDfbkgoBWgKFg4PwoeLnYzPY6RYNSs=
NEXTAUTH_SECRETとは

NextAuthがセッションデータを安全に保つために使用する秘密鍵。
このキーは、セッション情報を暗号化し、idTokenのハッシュ化に利用される。

なぜ必要なのか?

  • NextAuthは、ユーザーのセッション情報を安全に保つために、JWT(JSON Web Token)を使用している。
  • JWTは、ユーザーがログインしている間、ユーザーの情報を保持するためのトークンです。このトークンが安全に扱われないと、第三者が不正にアクセスするリスクがあります。
  • NEXTAUTH_SECRETを使うことで、以下のようなセキュリティが実現できます。
    • 暗号化: セッション情報を暗号化することで、トークンが盗まれたとしても内容を解読されにくくなります。
    • ハッシュ化: トークンのハッシュ化により、改ざんが検出しやすくなります。

どのように設定するのか?

  1. シークレットキーの生成
    シークレットキーは任意の値でも構いませんが、強力なキーを使用することでセキュリティが向上します。以下のコマンドをターミナルで実行すると、ランダムなシークレットキーを生成できます。
openssl rand -base64 32
  1. 環境変数に設定
  2. NextAuthの設定
JWTとは

JWT(JSON Web Token)は、ユーザーの認証情報を安全に転送するためのコンパクトでURLセーフなトークン形式です

JWTの基本構造

JWTは3つの部分から構成されます。それぞれがピリオド(.)で区切られています。

ヘッダー(Header)

トークンのタイプ使用されるアルゴリズムを指定します。

{
  "alg": "HS256",
  "typ": "JWT"
}

ペイロード(Payload)

ユーザーの情報やその他のデータを含みます。ここには、クレーム(claims)と呼ばれるステートメントが含まれます。例えば、ユーザーIDや発行者、発行日時など。

{
 "sub": "1234567890",
 "name": "John Doe",
 "iat": 1516239022
}

署名(Signature)

トークンの整合性を検証するために使用されます。ヘッダーとペイロードを連結し、秘密鍵でハッシュ化したものです。

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

JWTの使用例

JWTは、ユーザーがログインした後にサーバーがユーザーに返すトークン
このトークンをクライアントが保存し、後のリクエストに含めることで、サーバーはそのリクエストが認証されたユーザーからのものであることを確認できます。

JWTの役割とNextAuthでの利用

  1. 認証
    • ユーザーのログイン:
      *ユーザーがログインすると、サーバーはユーザーの情報を元にJWTを生成します。
      *生成されたJWTはクライアントに返され、クライアントはこのトークンを保存します。
  2. セッション管理
    *リクエストごとにトークンを送信:
    *クライアントは次回以降のリクエストでこのJWTをサーバーに送信します。通常はHTTPヘッダーに含めます
    *サーバーは受け取ったJWTを検証し、トークンが有効であればユーザーのリクエストを処理します。
  3. セキュリティ
    *トークンの署名
    JWTは署名されているため、トークンが改ざんされていないことを確認できます。
    *トークンの
    ペイロード部分は暗号化されていないため、誰でも内容を読めます*が、署名を検証することでトークンの整合性を確認できます。

NextAuthでのJWTの利用

NextAuthでは、セッション情報を保持するためにJWTを使用します。具体的には、ユーザーがログインすると、NextAuthはJWTを生成し、それをセッションに保存します。このトークンを使って、ユーザーの認証状態を確認します。

SessionProviderの作成

NextAuthはクライアントコードでセッションを閲覧(useSession()を利用)するために、SessionProviderでラップしておく必要があります
SessionProviderはクライアントコンポーネントで利用しなければいけないですが、Next.js 13のappディレクトリはデフォルトでサーバコンポーネントのため、任意のディレクトリにSessionProvider.tsxというコンポーネントを作成します。

クライアント側でセッションIDを持っているため、

src/provider/SessionProvider.tsx
'use client';

import { SessionProvider as NextAuthSessionProvider } from 'next-auth/react';

export interface SessionProviderProps {
  children: React.ReactNode;
}

const SessionProvider = ({ children }: SessionProviderProps) => {
  return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
};

export default SessionProvider;

Q:SessionProviderは'use client' ディレクティブを使用してクライアントコンポーネントとして定義されていますが、それを使ってアプリケーション全体をラップしちゃうと、アプリケーションがクライアントコンポーネントであるSessionProviderの子供になってアプリケーション全体がクライアントコンポーネントということにならないのですか?

**A:ならない。
**

SessionProvider が 'use client' ディレクティブを使ってクライアントコンポーネントとして定義されている場合、そのコンポーネントはブラウザでのみ実行されます。ただし、SessionProviderを使ってアプリケーション全体をラップすることが、アプリ全体をクライアントコンポーネントに変えるわけではありません。

SessionProvider は単にクライアント側でセッション情報を下位コンポーネントに渡すためのツールとして機能します。

結局のところ、アプリ全体を SessionProvider でラップするというのは、クライアント側のコンテキスト(この場合はユーザーセッションのデータ)をアプリケーションの全てのクライアントコンポーネントで利用可能にするという意味です

SessionProvider でアプリをラップすることは、実際には、アプリ内でセッション情報(ログインしたユーザーのデータなど)を扱えるようにする一種の「道具箱」をすべてのクライアント側コンポーネントに提供するようなものです。しかし、これがアプリ全体がクライアントサイド専用になるわけではありません。

SessionProvider をクライアントコンポーネントとして定義し、アプリケーション全体をラップすると、技術的にはアプリケーション全体がクライアントコンポーネントになります。しかし、Next.js の App Router では、これを特別に処理しています。
Next.js の App Router では、/app/layout.tsx は特別なファイルとして扱われます。このファイルは、アプリケーションのルートレイアウトを定義する役割を持ちます。layout.tsx 内で SessionProvider を使ってアプリケーション全体をラップしても、Next.js はこれを適切に処理し、サーバーコンポーネントとクライアントコンポーネントを区別して扱います。
具体的には、以下のようになります:
/app/layout.tsx 内の SessionProvider は、クライアント側で実行されるクライアントコンポーネントです。
layout.tsx のその他の部分やその子コンポーネントは、'use client' ディレクティブが使用されていない限り、サーバーコンポーネントとして扱われます。
layout.tsx の子コンポーネントの中で 'use client' ディレクティブを使用すると、そのコンポーネントはクライアントコンポーネントになります。

説明

  • Next.js の App Router では、layout.tsx がアプリケーションのレイアウトを定義する役割を持つため、layout.tsx ファイルが特別に扱われる。
  • layout.tsx 内で SessionProvider を使ってラップすると、技術的には SessionProvider の子要素がクライアントコンポーネントになります。しかし、Next.js の App Router は、これを特別に処理する。
  • Next.js は layout.tsx 内の SessionProvider を認識し、その子要素を適切にサーバーコンポーネントとクライアントコンポーネントに分離させる
  • Q:app直下じゃなくて、例えばapp/aaa/layout.tsxでも"特別な扱い"になりますか?
    • A:なる
  • Q:layout.tsxで、例えば他のロジックでラップしても、SessionProviderのように子要素が左右されることはありませんか?SessionProviderだけが特別ですか?
    • A:SessionProvide以外でもいける。layout.tsx ファイルにおいて、SessionProvider 以外のプロバイダーやラッパーを使用しても、同様の動作になります。Next.js の App Router は、layout.tsx 内のプロバイダーやラッパーを認識し、その子要素を適切にサーバーコンポーネントとクライアントコンポーネントに分離します。
oo

NextAuthの設定などの続き

ログイン部分作っていく。

  • Q:ログインロジックってどのディレクトリで行うのがいいの?conponents/elements/button/auth? features/auth/LoginButton.tsx?
    • A:features/auth/LoginButton.tsxこっち。
ディレクトリ構造
src/
  features/
    auth/
      api/
        login.ts
        logout.ts
        refreshToken.ts
      components/
        LoginButton.tsx
        LoginForm.tsx
      hooks/
        useAuth.ts
      types/
        index.ts

以下具体的に実装したもの

src/lib/firebase/auth.ts
import { GoogleAuthProvider, signInWithPopup, onAuthStateChanged as _onAuthStateChanged, User, UserCredential } from "firebase/auth";
import { auth } from "@/lib/firebase/client";

// 認証状態の変化を監視する関数
// ユーザーがサインインまたはサインアウトしたときにコールバック関数が呼び出される
export function onAuthStateChanged(cb: (user: User | null) => void) {
  return _onAuthStateChanged(auth, cb);
}

// Googleアカウントでサインインする関数
export async function signInWithGoogle(): Promise<UserCredential> {
  const provider = new GoogleAuthProvider();

  try {
    const userCredential = await signInWithPopup(auth, provider);
    return userCredential;
  } catch (error) {
    console.error("Googleでのサインイン中にエラーが発生しました", error);
    throw error; // エラーを呼び出し元に伝播
  }
}

// サインアウトする関数
export async function signOut() {
  try {
    return auth.signOut(); // Firebaseのサインアウトメソッドを呼び出す
  } catch (error) {
    console.error("サインアウト中にエラーが発生しました", error); // サインアウトに失敗した場合のエラーハンドリング
    throw error; // エラーを呼び出し元に伝播
  }
}
src/features/auth/types/index.ts
export interface User {
  id: string;
  name: string | null;
  email: string | null;
  image: string | null;
}
src/features/auth/store/authStore.ts
import { create } from "zustand";
import { User } from "../types";

interface AuthState {
  user: User | null;
  setUser: (user: User) => void;
  clearUser: () => void;
}

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  setUser: (user) => set({ user }),
  clearUser: () => set({ user: null }),
}));
src/features/auth/hooks/useSignin.ts
import { signInWithGoogle } from "@/lib/firebase/auth";
import { signIn as signInByNextAuth } from "next-auth/react";

export const useSighIn = () => {
  const signIn = async () => {
    try {
      const userCredential = await signInWithGoogle();
      const idToken = await userCredential.user.getIdToken();
      await signInByNextAuth("credentials", {
        idToken,
        callbackUrl: "/",
      });
    } catch (error) {
      console.error("ログイン処理中にエラーが発生しました", error);
    }
  };

  return { signIn };
};
src/features/auth/components/SignInButton.tsx
"use client";

import { useSignIn } from "../hooks/useSignIn";

const LoginButton = () => {
  const { signIn } = useSignIn();

  return (
    <button type="button" onClick={signIn}>
      Googleでログイン
    </button>
  );
};

export default LoginButton;

ポイント
*lib/firebase/auth.tsでのgoogleログイン関数の返り値を設定

  • hooksにログインロジックを切り出して、それをconponentsで使用するように。
  • hooksでは、記事の通り、firebase認証でゲットしたidTokenをもとにNextAuthでログインする処理を書いた。

セッション管理と状態管理

状態管理ライブラリとしてZustandを採用。
その場合のNextAuthとZustandの責務を明確にしたい。

  • NextAuthの責務
    • セッション管理
  • Zustandの責務
    • ログイン状態の管理(ただし、信頼できる情報源はNextAuth。あくまでUI側の利用の時にstoreを使用する。)

features/auth/store/authStore.tsこんな感じでディレクトリを使うことにする。

oo

NextAuthの設定などの続き part2

/app/api/auth/[...nextauth].ts

/app/api/auth/[...nextauth].ts
// # 処理フロー
// ## このファイルの前段階
// (クライアントサイド:ログインが行われるとFirebaseでユーザー認証が行われ、idTokenが発行される)
// (クライアントサイド -> サーバーサイド:クライアントサイドで発行されたidTokenをNextAuthに渡す)
// ## このファイル内
// 1. サーバーサイド:Next Authの設定ファイルの中で、Firebaseのadminを利用してidTokenの検証が行われる。 
// 2. サーバーサイド:検証が成功したらFirebaseからidTokenが持つ情報(Email, UID、トークンを発行した認証サーバー、発行日時、有効期限)が取り出される。
// 3. サーバーサイド:その情報をJWTというトークン形式で、渡される。
// 4.JWTからユーザー情報(Email、UID)を取り出してセッションに格納。(セッションはサーバーサイドのもの。セッションIDはクライアントサイドのもの)
// #一問一答
//  * Q:結局このファイルって何?
// A:結局、`export default NextAuth(authOptions);`で、authOptionsを用いてNextAuthの設定をするファイル。
// * Q:どこで動くの?
//A:Next.jsのAPIルートはサーバーサイドで実行される。このファイルは`app/api/auth~`なので当然API。
// * Q:CredentialsProviderとは何?
// A: Next Authが提供する認証プロバイダの一つ。https://next-auth.js.org/configuration/providers/credentials。ユーザー名とパスワード、2 要素認証などの任意の資格情報を使用してサインインを処理できます。
// * Q:[...nextauth]とは?
// A:Dynamic Routeでの書き方。/api/auth/以下のすべてのルートが、このroute.tsファイルで処理される。

import NextAuth from "next-auth";
import type { NextAuthOptions } from "next-auth";

import CredentialsProvider from "next-auth/providers/credentials";

import { auth } from "@/firebase/admin";

import { JWT } from "next-auth/jwt"; // JWT型をインポート
import "@/features/auth/types/next-auth"; 

export const authOptions: NextAuthOptions = {
  providers: [
    CredentialsProvider({
      credentials: {}, // NextAuthでログインフォームを作る時に使用する
      authorize: async ({ idToken }: { idToken: string }, _req) => { //ここ修正
        // authorizeには受け取ったidTokenを検証しユーザー情報を返すロジックを書く必要がある。
        // ここではクライアントから受け取ったidTokenを検証し、ユーザ情報を取得している
        if (idToken) {
          try {
            // firebaseのadminでidTokenを検証。成功したらユーザー情報(decoded)を返す。
            const decoded = await auth.verifyIdToken(idToken);

            return { ...decoded };
          } catch (err) {
            console.error(err);
          }
        }
        return null;
      },
    }),
  ],
// セッションの保存形式をJWTと定める。
  session: {
    strategy: "jwt",
  },
// JWTトークンからユーザー情報を取得してセッションに格納するための関数。
  callbacks: {
    async jwt({ token, user }: { token: JWT; user?: any }) { //ここ修正
      // jwtコールバック
      // jwtコールバックは、JWTトークンが作成または更新されるたびに呼び出されます。
      // authorizeコールバックで取得したユーザ情報をJWTトークンに含める処理
      return { ...token, ...user };
    },
    async session({ session, token }) {
      // sessionコールバック
      // sessionコールバックは、セッションが作成または更新されるたびに呼び出されます。
      // JWTトークンのユーザー情報(Email,UID)を取り出してセッションに格納する。
      session.user.emailVerified = token.emailVerified;
      session.user.uid = token.uid;
      return session;
    },
  },
};

export default NextAuth(authOptions);

/pages/api/auth/[...nextauth].tsの説明。

  • NextAuthでクライアントからサーバー側に送られたidTokenを検証し、セッションに格納する設定をするファイル

  • CredentialsProvider

    • サーバー側にいるNextAuthは、CredentialsProviderを使用して、クライアントから受けとったidTokenを検証。
    • auth.verifyIdToken
      • Firebaseのadmin(auth)で登録されているユーザーかどうかを確認する。(auth.verifyIdToken)
      • auth.verifyIdTokenは Firebase Admin SDKが提供する関数の一つです。この関数は、クライアントから受け取ったFirebaseのIDトークン(idToken)を検証するために使用されます。auth.verifyIdToken関数は、受け取ったIDトークンが有効であるかどうかを検証します。検証に成功した場合、auth.verifyIdToken関数は、トークンに含まれるユーザ情報をデコードして返します。返されるユーザ情報にはユーザID(uid)メールアドレス表示名などが含まれます。したがって、auth.verifyIdToken関数を使うことで、クライアントから受け取ったIDトークンが信頼できるものであるかどうかを確認し、トークンに含まれるユーザ情報を安全に取得することができます。

  • authorizeコールバックで、受け取ったidTokenをFirebaseの管理SDK(admin)で検証。検証が成功したらユーザー情報を返す。

これはNext Authの設定ファイルです。
CredentialsProviderを使用して、クライアントから送信されたidTokenを検証するための設定を行います。
authorizeコールバックでは、受け取ったidTokenをFirebaseの管理SDKを使用して検証し、検証が成功した場合はユーザー情報を返します。
sessionの設定で、セッション情報の保存方法を指定します。ここでは"jwt"を使用しています。
callbacksの設定では、JWTトークンからユーザー情報を取得し、セッション情報に格納するためのコールバック関数を定義します。
jwtコールバックでは、authorizeで返されたユーザー情報をJWTトークンに含めます。
sessionコールバックでは、JWTトークンからユーザー情報を取得し、セッション情報に格納します。
これらの設定と処理の流れは以下のようになります:
ユーザーがログイン画面でメールアドレスとパスワードを入力し、ログインボタンを押します。
Firebase SDKを使用してユーザー認証が行われ、idTokenが取得されます。
取得したidTokenをNext AuthのsignInByNextAuth関数に渡して、認証セッションを確立します。
Next Authの設定ファイルで、CredentialsProviderを使用してidTokenの検証が行われます。
検証が成功すると、ユーザー情報がJWTトークンに含められ、セッション情報に格納されます。
アプリケーションコードでuseSession()やgetServerSession()を使用して、セッション情報からユーザー情報を取得することができます。

型ファイル

@types/next-auth.d.ts
import NextAuth, { DefaultSession } from "next-auth";
import { JWT } from "next-auth/jwt";

declare module "next-auth" {
  interface Session {
    user: {
      // Firebaseの認証情報
      uid: string;
      emailVerified?: boolean;
    } & DefaultSession["user"];
  }
}

declare module "next-auth/jwt" {
  interface JWT {
    // Firebaseの認証情報
    uid: string;
    emailVerified: boolean;
  }
}

型定義のパスを通すために

tsconfig.json

    "typeRoots": ["./src/types"],

これを追加。
また、d.tsはライブラリのための型定義ファイルで、これは普通の型定義と区別して、src直下のtypesに格納する。
その上で、使用したいファイルでは読み込む必要がない。

route

src/app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import { authOptions } from "./auth-options";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };
oo

ユーザーアイコン画像のパスやユーザー名が取得できない問題。

ちょっとわからんけど使わないから一旦置いとく。