Firebaseに作成したReactのアプリにメールとパスワードで登録したユーザーの退会処理にフォーカスを当てて解説します。
※ ソースはこちら:Github
※ アプリはこちら:「Firebaseに認証するだけのアプリ」
※ パッケージのバージョン:”firebase”: “^9.22.1″、”react”: “^18.2.0″ (詳細はGithubのpackage.jsonを参照してください。)
※ Firebaseの認証周り全般の解説はこちらを参照してください。
プログラムの多くはそうなんですが、「後始末」に類する処理は結構大変なんです。
ということでサインアップ処理よりもユーザーの退会処理の方がいろいろ大変。
ただ、ちゃんと後始末しないと、ユーザ登録数がパンク…なんてことも起きるんですよね。
ホーム画面
ホーム画面は、
によって構成されています。
// Home.js
import { signOut } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { createPortal } from "react-dom";
import { HomePasswordChangeModal, HomeWithdrawalChild } from "./HomeChild";
import { auth } from "../service/firebase/firebase";
import { useAuthContext } from "../context/AuthContext";
import Title from "./Title";
import { Button } from "@mui/material";
import { Logout } from "@mui/icons-material";
import PersonRemoveIcon from "@mui/icons-material/PersonRemove";
import PublishedWithChangesIcon from '@mui/icons-material/PublishedWithChanges';
const ModalPortal = ({ children }) => {
const target = document.querySelector(".modalContainer");
return createPortal(children, target);
};
const Home = () => {
const [withdrawalModalOpen, setWithdrawalModalOpen] = useState(false);
const [passwordChangeModalOpen, setPasswordChangeModalOpen] = useState(false);
const navigate = useNavigate();
const { user } = useAuthContext();
const handleLogout = () => {
signOut(auth);
navigate("/login");
};
return (
<>
<div>
<Title />
<div className="modalContainer"></div>
<p>現在ログインしているユーザーの情報</p>
<p>email : {user.email}</p>
<p>
Provider :
{user.providerData[0].providerId === "password"
? "email Login"
: user.providerData[0].providerId === "google.com"
? "Google"
: "invalid provider"}
</p>
<p>
{!user.emailVerified &&
"アドレス未認証のためTodoは入力できません。(メール認証が終了したら、リロードしてください。)"}
</p>
<Button
onClick={handleLogout}
variant="outlined"
startIcon={<Logout />}
sx={{ mr:1 }}
>
ログアウト
</Button>
<Button
onClick={() => setWithdrawalModalOpen(true)}
variant="outlined"
startIcon={<PersonRemoveIcon />}
sx={{ mr:1 }}
>
退会
</Button>
{user.providerData[0].providerId === "password"
&& <Button onClick={() => setPasswordChangeModalOpen(true)}
variant="outlined"
startIcon={<PublishedWithChangesIcon />}
>
パスワード変更
</Button> }
</div>
{withdrawalModalOpen && (
<ModalPortal>
<HomeWithdrawalChild
setWithdrawalModalOpen={setWithdrawalModalOpen}
/>
</ModalPortal>
)}
{passwordChangeModalOpen && (
<ModalPortal>
<HomePasswordChangeModal
setPasswordChangeModalOpen={setPasswordChangeModalOpen}
/>
</ModalPortal>
)}
</>
);
};
export default Home;
// HomeChild.js
import { signOut } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { createPortal } from "react-dom";
import { HomePasswordChangeModal, HomeWithdrawalChild } from "./HomeChild";
import { auth } from "../service/firebase/firebase";
import { useAuthContext } from "../context/AuthContext";
import Title from "./Title";
import { Button } from "@mui/material";
import { Logout } from "@mui/icons-material";
import PersonRemoveIcon from "@mui/icons-material/PersonRemove";
import PublishedWithChangesIcon from '@mui/icons-material/PublishedWithChanges';
const ModalPortal = ({ children }) => {
const target = document.querySelector(".modalContainer");
return createPortal(children, target);
};
const Home = () => {
const [withdrawalModalOpen, setWithdrawalModalOpen] = useState(false);
const [passwordChangeModalOpen, setPasswordChangeModalOpen] = useState(false);
const navigate = useNavigate();
const { user } = useAuthContext();
const handleLogout = () => {
signOut(auth);
navigate("/login");
};
return (
<>
<div>
<Title />
<div className="modalContainer"></div>
<p>現在ログインしているユーザーの情報</p>
<p>email : {user.email}</p>
<p>
Provider :
{user.providerData[0].providerId === "password"
? "email Login"
: user.providerData[0].providerId === "google.com"
? "Google"
: "invalid provider"}
</p>
<p>
{!user.emailVerified &&
"アドレス未認証のためTodoは入力できません。(メール認証が終了したら、リロードしてください。)"}
</p>
<Button
onClick={handleLogout}
variant="outlined"
startIcon={<Logout />}
sx={{ mr:1 }}
>
ログアウト
</Button>
<Button
onClick={() => setWithdrawalModalOpen(true)}
variant="outlined"
startIcon={<PersonRemoveIcon />}
sx={{ mr:1 }}
>
退会
</Button>
{user.providerData[0].providerId === "password"
&& <Button onClick={() => setPasswordChangeModalOpen(true)}
variant="outlined"
startIcon={<PublishedWithChangesIcon />}
>
パスワード変更
</Button> }
</div>
{withdrawalModalOpen && (
<ModalPortal>
<HomeWithdrawalChild
setWithdrawalModalOpen={setWithdrawalModalOpen}
/>
</ModalPortal>
)}
{passwordChangeModalOpen && (
<ModalPortal>
<HomePasswordChangeModal
setPasswordChangeModalOpen={setPasswordChangeModalOpen}
/>
</ModalPortal>
)}
</>
);
};
export default Home;
ホーム画面の退会ボタン
ホーム(メイン)画面に「退会」ボタンを作成しました。
// Home.jsから抜粋
~
<Button
onClick={() => setWithdrawalModalOpen(true)}
variant="outlined"
startIcon={<PersonRemoveIcon />}
sx={{ mr:1 }}
>
退会
</Button>
~
「退会」ボタンをクリックすると、withdrawalModalOpenがsetWithdrawalModalOpenによってtrueになり、退会のためのモーダル画面が表示されます。
「退会」のためのモーダル画面出力
「退会」のモーダル画面には、退会を確認するためのボタンがあります。
「本当に退会する」ボタンをクリックすると、退会処理が起動します。
// HomeChild.js抜粋
export const HomeWithdrawalChild = ({ setWithdrawalModalOpen }) => {
const navigate = useNavigate();
const user = auth.currentUser;
const handleDelUserClick = () => {
delUser().then((res) => {
setWithdrawalModalOpen(false);
res && user.providerData[0].providerId === "password"
? navigate("/SignUp")
: navigate("/Login");
});
};
return (
<div className="modal">
<div className="modal__content">
<p>本当に退会しますか?</p>
<div>
<button type="button" onClick={handleDelUserClick}>
本当に退会する
</button>
</div>
<div>
<button type="button" onClick={() => setWithdrawalModalOpen(false)}>
やっぱり退会しない
</button>
</div>
</div>
</div>
);
};
「本当に退会する」ボタンをクリックすると、handleDelUserClick()の中で、delUser()がコールされます。
delUser()で退会のためにアプリからのユーザー削除処理が行われ、成功すると、サインアップ画面に移行します。
退会処理:アプリからメールとパスワードで登録したユーザーを削除する
アプリからメールとパスワードで登録したユーザーを削除する処理を以下に解説します。
delUser.js:ユーザー削除処理
// delUser.js
import {
deleteUser,
} from "firebase/auth";
import { auth, googleProvider } from "../firebase/firebase";
import getCredential from "../firebase/getCredential";
import { issueMsg } from "../common/issueMsg";
import loginFirebase from "../firebase/loginFirebase";
export const delUser = async () => {
const user = auth.currentUser;
let loginProvider;
try {
// ここで、ログインしているプロバイダーを判断する
if (user.providerData[0].providerId === "password") {
loginProvider = "email";
} else if (user.providerData[0].providerId === "google.com") {
loginProvider = "google";
/******* Google認証の退会処理開始 *******/
// 退会用のログイン
const currentLogonUserEmail = user.email;
issueMsg(
"ログインしているGoogle IDで再ログインしてください",
currentLogonUserEmail
);
try {
// ポップアップウインドウが表示
// await signInWithPopup(auth, googleProvider);
await loginFirebase("google",googleProvider);
const newUser = auth.currentUser;
const newUserEmail = newUser.email;
await deleteUser(newUser);
if (newUserEmail !== currentLogonUserEmail) {
issueMsg(
"再ログインしたGoogleIdが異なります。再度ログインして退会処理をやり直してください。誤ログインEmail:",
newUserEmail
);
return false;
}
issueMsg("退会処理が完了しました。:", newUser.email);
return true;
} catch (error) {
if (error.code === "auth/popup-closed-by-user") {
issueMsg(
"退会処理のためのGoogleIdでのログインがキャンセルされました。"
);
return false;
} else {
console.log(error.code);
issueMsg(error.message);
return false;
}
}
/****** Google認証の退会処理終了 *******/
} else {
issueMsg(
"退会処理で不当なプロバイダーが指定された。",
user.providerData[0].providerId
);
return false;
}
const result = await getCredential(loginProvider, user);
if (!result) {
return false;
}
await deleteUser(user);
issueMsg("退会処理が完了しました。:", user.email);
return true;
} catch (error) {
if (error.code === "auth/wrong-password") {
issueMsg("入力したパスワードが不正です。");
} else {
issueMsg(`退会処理で不正が発生しました。`, error.code);
}
console.log(`退会失敗`);
console.log(user);
console.log(error);
console.log(error.message);
console.log(error.code);
return false;
}
};
メールアドレスとパスワードでユーザー登録したユーザーと、Google IDでユーザー登録したユーザーのユーザー削除処理両方が含まれています。
アプリからユーザーを削除するのに、単にdeleteUser()を発行すると、「auth/requires-recent-login」というエラーでFirebaseに怒られます。
意味は、
Thrown if the user’s last sign-in time does not meet the security threshold. Use firebase.User.reauthenticateWithCredential to resolve. This does not apply if the user is anonymous.
「ユーザの最終サインイン時刻がセキュリティ閾値を満たしていない場合にスローされます。firebase.User.reauthenticateWithCredential を使用して解決します。ユーザが匿名の場合は適用されません。」
英文は、Firebaseのドキュメント、和文はDeepL翻訳で和訳しました。
これはどういうことかというと、下記の理由からです。
アカウントの削除、メインのメールアドレスの設定、パスワードの変更といったセキュリティ上重要な操作を行うには、ユーザーが最近ログインしている必要があります。
『Firebase でユーザーを管理する』から引用
簡単に言うと、ログインしてからある程度時間がたつと再度ログインするか、ユーザーの再認証をしないと「アカウントの削除、メインのメールアドレスの設定、パスワードの変更」等はエラーになるよ、ということです。
ということで、削除(deleteUser())する前に再認証処理(getCredential()をコール)というものが必要になります。
getCredential()をコールして、ユーザーの再認証が成功すると、メッセージを出力してメールアドレスとパスワードでユーザー登録したユーザーの退会処理(削除処理)が終了します。
getCredential.js:再認証処理
// getCredential.js
import {
reauthenticateWithCredential,
EmailAuthProvider,
} from "firebase/auth";
const getCredential = async (type, user, pass = null) => {
let credential;
if (type === "email") {
let password;
pass
? (password = pass)
: (password = prompt("パスワードを入力してください"));
if (!password) {
return false;
}
// email用Credentialを求める
credential = EmailAuthProvider.credential(user.email, password);
} else if (type === "google") {
// 現時点では、プロバイダーがGoogleの時には、当該関数はコールされない。
} else {
console.log("不当な認証情報要求です");
return false;
}
await reauthenticateWithCredential(user, credential);
return true;
};
export default getCredential;
getCredential.jsではGoogle IDでユーザー登録したユーザーの判定がありますが、実際はGoogle IDのユーザー削除処理では当該モジュールはコールされません。
ユーザーがメールアドレスとパスワードでユーザー登録していた場合、
password = prompt(“パスワードを入力してください”)
ログインの再認証に必要なreauthenticateWithCredential()のパラメータであるcredentialを求めるEmailAuthProvider.credential()の発行のためにパスワードが必要になります。
そこで、prompt()によってパスワードの入力を求めます。
EmailAuthProvider.credential()
promptで求めたパスワードを元にEmailAuthProvider.credential()でcredentialを求めます。
EmailAuthProviderはメールアドレスとパスワードでユーザー登録したユーザーのcredentialを求める時に使います。
「credential」とは、firebaseの認証に関していろいろと定義してある証明書のようなもののようです。その程度の認識でいいんじゃないのでは。
reauthenticateWithCredential()
reauthenticateWithCredential()にユーザー情報、credentialをセットすれば、再認証が完了してdelUserに正常にリターンします。
最後に
メールアドレスとパスワードでユーザー登録したユーザーのユーザー登録の削除処理に関しては、再認証処理の部分が結構面倒です。
ログインして、即退会処理をするのであれば、再認証は必要ありません。
しかし、ログインしていろいろ操作して、退会が必要になれば、その時点から退会ということになるのでしょうから、再認証処理は必須です。
Firebaseの認証処理ではこの退会の部分が一番複雑ですのでじっくりソースコードを見てほしいと思います。
当該Reactのプロジェクトのsrc配下のファイル構造
以下が、当該Reactのプロジェクトのsrc配下のファイル構造です。
SRC
│ App.css
│ App.js
│ App.test.js
│ index.css
│ index.js
│ reportWebVitals.js
│ service-worker.js
│ serviceWorkerRegistration.js
│ setupTests.js
│
├─components
│ │ Home.js
│ │ HomeChild.js
│ │ Login.js
│ │ PrivateRoute.js
│ │ PublicRoute.js
│ │ SignUp.js
│ │ Title.js
│ │
│ └─styles
│ portal.css
│
├─context
│ AuthContext.js
│
└─service
├─authenticationProcess
│ changePassword.js
│ delUser.js
│
├─common
│ issueMsg.js
│
└─firebase
firebase.js
getCredential.js
loginFirebase.js
registration.js
sendEmailLink.js
submitPasswordResetEmail.js
このブログに掲載されているコードは、著作権者(私)に帰属します。コードを自由に使用、複製、改変、配布、販売、サブライセンスできます。また、コードを使用した結果、何らかの損害が発生した場合でも、著作権者は一切責任を負いません。