👋
(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形式)
- リフレッシュトークン(トークンの期限が切れたときに更新するためのもの)
保存場所の確認方法
- ブラウザでアプリを開いた状態で、開発者ツールを開く(
F12
キーや右クリック → 「検証」) -
Application
タブ →IndexedDB
→firebaseLocalStorageDb
を選択 - 中にある
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