👋

(Firebase) JWTログインからFirebase認証への移行

に公開

FirebaseのStorageに写真を保存する際に、ログインしたユーザーだけが登録できるようにルールを設定していました。しかし、一つ問題がありました。
私が作成したログイン方法はJWT方式で、FirebaseはそのJWTトークンを認識できません。
そのため、FirebaseのAuthenticationを使ってログイン機能を実装し直すことにしました。
(アカウント作成もこちらで行います)

✅ 今後の対応内容

🔸 アカウント作成

  • パスワードはFirebaseが自動的に安全に管理するため、データベースに保存する必要がなくなります
  • ユーザーデータの構造を変更するため、DBの修正が必要です(例:UIDやメールアドレスのみ保存)

🔸 JWTログインからFirebase認証への移行

  • これまで使用していたJWTベースのログイン認証処理は不要になります
  • サーバー側では、Firebaseが発行するIDトークンを検証して、ユーザー認証を行うように変更します
  • IDトークンの検証にはFirebase Admin SDKを使用予定です
項目 変更前(独自JWT認証) 変更後(Firebase Authentication)
アカウント作成 パスワードを自前でハッシュ化しDBに保存 FirebaseのcreateUserWithEmailAndPassword()で作成しDBにはUIDのみ保存
パスワード管理 DBにハッシュ保存 Firebaseが安全に管理
ログイン処理 独自JWTを発行しクライアント管理 Firebase AuthのsignInWithEmailAndPassword()利用しIDトークン取得
サーバー側認証検証 独自JWTの検証ロジック Firebase Admin SDKを使いIDトークンを検証
DBユーザー情報設計 email, password (hashed) 等保存 Firebase UID, email, nickname など必要最低限の情報を保存
Storage/Firestoreルール JWTトークンを認識できず不整合 Firebaseのauth.uidを基準にルール設定
APIリクエスト認証 独自JWTをAuthorizationヘッダーにセット FirebaseのIDトークンをAuthorizationヘッダーにセット

🔸 アカウント作成

firebase SDKでのアカウント作成(クライアント側)

  • Firebase Authenticationの関数 createUserWithEmailAndPassword(auth, email, password) を使う

  • 成功すると user.uid(Firebaseが割り当てたユニークID)を取得可能


// Firebase Authentication アカウントを取得
// createUserWithEmailAndPassword(auth, email, password) Firebase 関数
// アカウントがさせれたら、ユーザー情報を取得(user)
import { createUserWithEmailAndPassword } from "firebase/auth";
import { auth } from "../firebase"; 

export async function signup(email: string, password: string){
    try {
        // Firebase Authenticationを使用してユーザーを作成
        const userCredential = await createUserWithEmailAndPassword(auth, email, password);
    
        // ユーザー情報を取得
        const user = userCredential.user;
        
        // ユーザーのメールアドレスやUIDなどを返す
        //  必要に応じて、ユーザープロフィールの設定やデータベースへの保存などを行う
        return {
            email: user.email,
            uid: user.uid,
        };
    } catch (error) {
        console.error("Signup error:", error);
        throw error; // エラーを再スローして呼び出し元で処理できるようにする
    }


}

アカウント作成のコンポーネント

  • ユーザーから入力を受け取り(JoinFrom)
  • Firebaseにアカウントを作成し
  • そのUIDをサーバーのDBに送信してユーザー情報を登録する

"use client";
import { useState } from "react";   
import { signup} from "@/lib/auth/signup";
import { joinForm } from "@/types/models/user";

// PWはDBに保存しないので、DB修正必要

export default function SignupForm() {

    const [joinData, setJoinData] = useState<joinForm>({
        nickname: "",
        email: "",
        password: "",
    });

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        try {
            // Firebase Authenticationを使用してユーザーを作成
            if (!joinData.email || !joinData.password || !joinData.nickname) {
                return alert("すべての項目を入力してください。");
            }
            const user = await signup(joinData.email, joinData.password);
            if (user){
                alert("Signup Success");
                console.log("Signup Success", user);
            }

            // ユーザー情報をサーバーに送信
            const response = await fetch("/api/join", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify({
                    nickname: joinData.nickname,
                    email: joinData.email,
                    uid: user.uid, // Firebaseから取得したuidを使用
                }),
            });
            const data = await response.json();
            if (response.ok) {
                alert("Join Success");
                console.log("Join Success", data);  
            }
            else {
                alert("Join Fail");
                console.log("Join Fail", data); 
            }
            }catch (err) {
                console.error("Error in handleSubmit", err);
                alert("Signup Fail");   
        }
    };


    return(
        <div>
            <h2>アカウント作成</h2>
            <form onSubmit={handleSubmit}>
                省略
            </form>
        </div>
    )
}

サーバー側API

  • クライアントから送られた uid, email, nickname を受け取りDBに保存(JoinRequestData)
  • パスワードの保存は不要(Firebaseで管理しているため)
import prisma from '@/lib/prisma';
import { JoinRequestData } from "@/types/models/user";
import { NextResponse } from "next/server";

//Join
export async function POST(req:Request) {
        try{
    const {nickname, uid , email} : JoinRequestData = await req.json();
    console.log("joinForm", nickname, uid, email);

    const result = await prisma.user.create({
        data : {
            nickname,
            email,
            uid
        }
    });

    console.log("result", result);
    return NextResponse.json({
        message : "Join Success", result},{status : 201})    
    }catch(err){
        console.error("Error in join", err);
        return NextResponse.json({
            message : "Join Fail"}, {status : 500});
    }

}

🔸JWTログインからFirebase認証への移行

ログインの流れ

[1] フロントエンド

  • ユーザーがメールとパスワードを入力
  • Firebase Auth にログイン → IDトークンを取得
  • IDトークンを Authorization ヘッダーに付けてサーバーに送信

[2] バックエンド

  • Firebase Admin SDK で IDトークンを検証
  • ユーザーのUIDを取得し、認証済みとして処理を続ける

Firebaseログイン関数作成

  • ログインはsignInWithEmailAndPassword(auth, email, password)使用
  • 成功すると、tokenをゲット
  • TokenとUid、Emailを返す
// Firebase Authenticationを使用してログイン
import { signInWithEmailAndPassword } from "firebase/auth";
import { auth } from "@/lib/firebase"; // Firebaseの初期化を行うファイル

export async function login(email: string, password: string) {
    try {
        // Firebase Authenticationを使用してログイン
        const userCredential = await signInWithEmailAndPassword(auth, email, password);
        
        // ユーザー情報 / token 取得
        //必要に応じて、トークンをサーバーに送信してセッション管理などを行うことができます。
        const user = userCredential.user;
        const token = await user.getIdToken();
        
        // ユーザーのメールアドレスやUIDなどを返す
        return {
            email: user.email,
            uid: user.uid,
            token
        };
    } catch (error) {
        console.error("Login error:", error);
        throw error; // エラーを再スローして呼び出し元で処理できるようにする
    }
}

Firebase Admin SDKの初期設定

  • Firebaseコンソールで「サービスアカウント」から秘密鍵(JSONファイル)をダウンロード
  • JSONファイルから必要な情報(project_id、client_email、private_key)を環境変数に設定
  • private_keyの改行コードは \n を改行文字に置換してください(必須)
// IDトークンを検証
import { initializeApp, cert, getApps } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';

const adminConfig = {
  projectId: process.env.FIREBASE_PROJECT_ID,
  clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
  privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
};

if (!getApps().length) {
  initializeApp({
    credential: cert(adminConfig),
    // storageBucket: 必要に応じて storageBucket も追加可能
  });
}

export const adminAuth = getAuth(); // // IDトークン検証用にエクスポート

  • 環境変数はサーバー専用で管理し、NEXT_PUBLIC_ プレフィックスを付けないでください。
  • NEXT_PUBLIC_付きの変数はクライアント側で見えてしまいます。

ログインコンポーネント

"use client"

import { useState } from "react";
import { login } from "@/lib/auth/login";
import { loginForm } from "@/types/models/user";

export default function SigninForm() {
    const [loginData, setLoginData] = useState<loginForm>({
        email: "",
        password: "",
    })

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { name, value } = e.target;
        setLoginData({
            ...loginData,
            [name]: value
        });
    };

    const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
        try {
            // Firebase Authenticationを使用してログイン
            if (!loginData.email || !loginData.password) {
                return alert("メールアドレスとパスワードを入力してください。");
            }
            const user = await login(loginData.email, loginData.password);
            if (user) {
                console.log("Login Success", user);
            }

            // サーバーにログインリクエストを送信
            if (!user.token) {
                return alert("トークンが取得できませんでした。ログインに失敗しました。");
            }

            const response = await fetch("/api/auth/login", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    Authorization: `Bearer ${user.token}`, // ⭐️トークンをヘッダーに追加

                },});

                const data = await response.json();
            if (response.ok) {
                alert("Login Success");
                console.log("Login Success", data);
            }else {
                alert("Login Fail");
                console.log("Login Fail", data);
            }
                
        } catch (err) {
            console.error("Error in handleSubmit", err);
            alert("Login Fail");
        }
    };

    return (
        <div>
            省略
        </div>
    );
}

サーバー側API((IDトークン検証))

  • Admin ADKの認証機能使う
  • headersからトークン取得
  • トークンを使ってuid取得
  • uidを使ってDBからユーザー情報取得
import { NextResponse, NextRequest } from "next/server";
import { initializeApp } from "firebase-admin";
import { getAuth } from "firebase-admin/auth"; //admin SDKの認証機能を使用

initializeApp(); // Firebase Admin SDKの初期化

export async function GET(req: NextRequest) {
    const token = req.headers.get("Authorization")?.split(" ")[1];
    if (!token) {
        return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
    }

    try{
        const decodedToken = await getAuth().verifyIdToken(token);
        const uid = decodedToken.uid;

        
        const user = await prisma?.user.findUnique({
            where: { uid: uid },
            select: {
                uid: true,
                email: true,
                nickname: true,
        }});

        if (!user) {
            return NextResponse.json({ message: "User not found" }, { status: 404 });
        }
        return NextResponse.json({ user }, { status: 200 });
    }catch (error) {
        console.error("Error verifying token:", error);
        return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
    }

}

🔐 Firebaseのログイン情報はどこに保存されているの?

Firebase Authentication を使ってログインすると、ログイン情報は自動的にブラウザに保存されます。これは特に開発者にとって便利な仕組みです。

🔸 どこに保存されるの?

Firebaseは、ブラウザの IndexedDB という領域にデータを保存します。
具体的には以下のようになります:

  • データベース名firebaseLocalStorageDb
  • オブジェクトストア名firebaseLocalStorage

🔸 なぜ IndexedDB に保存されるの?

IndexedDBは、ローカルに構造化されたデータを保存できるブラウザの仕組みで、大容量のデータにも対応しています。Firebaseはこの仕組みを利用して、以下のような情報を保持しています:

  • 現在ログイン中のユーザー情報
  • FirebaseのIDトークン(JWT形式)
  • リフレッシュトークン(トークンの期限が切れたときに更新するためのもの)

保存場所の確認方法

  1. ブラウザでアプリを開いた状態で、開発者ツールを開く(F12キーや右クリック → 「検証」)
  2. Applicationタブ → IndexedDBfirebaseLocalStorageDb を選択
  3. 中にある firebaseLocalStorage を開くと、保存されているデータを確認できます

保存された情報の活用

Firebaseでは、保存された情報を以下のように簡単に取得できます:

import { getAuth } from "firebase/auth";

const auth = getAuth();
const user = auth.currentUser;

if (user) {
  const token = await user.getIdToken();
  // このトークンをAPIリクエストに使う
}

🔸 補足

  • Firebaseが自動で管理してくれるため、localStorageやsessionStorageに手動で保存する必要はありません。
  • アプリを再読み込みしてもログイン状態が維持されます。
  • サインアウトすると、この情報も自動で削除されます。

Discussion