Firebaseに作成したReactのアプリにGoogle IDで登録したユーザーの退会処理にフォーカスを当てて解説します。
※ ソースはこちら:Github
※ アプリはこちら:「Firebaseに認証するだけのアプリ」
※ パッケージのバージョン:”firebase”: “^9.22.1″、”react”: “^18.2.0″ (詳細はGithubのpackage.jsonを参照してください。)
※ Firebaseの認証周り全般の解説はこちらを参照してください。
プログラムの多くはそうなんですが、「後始末」に類する処理は結構大変なんです。
ということでサインアップ処理よりもユーザーの退会処理の方がいろいろ大変。
メールアドレスとパスワードでユーザー登録したユーザーの退会処理よりもGoogle IDで登録したユーザの退会処理は大変でした。
ホーム画面
ホーム画面は、
によって構成されています。
// 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()で退会のためにアプリからのユーザー削除処理が行われ、成功すると、サインアップ画面に移行します。
退会処理:アプリからGoogle IDで登録したユーザーを削除する
アプリからGoogle IDで登録したユーザーを削除する処理を以下に解説します。
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;
}
};
delUser.jsにはメールとパスワードで登録したユーザーと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())する前に再認証処理というものが必要になります。
Google IDで登録したユーザーの再認証処理
Google IDで登録したユーザーの場合は、ユーザーに退会処理の前に再度同じGoogle IDでログインしてもらい、その後ユーザ情報を削除しました。
// delUser.js抜粋
~
const currentLogonUserEmail = user.email;
issueMsg(
"ログインしているGoogle IDで再ログインしてください",
currentLogonUserEmail
);
~
まず、現在ログオンしているGoogle IDに対応するemailアドレスをユーザ情報から抽出。
そして、ポップアップメッセージにそのemailアドレスを表示。
再認証のための再ログインのポップアップ表示をします。
// delUser.js抜粋
~
await loginFirebase("google",googleProvider);
~
※ loginFirebaseの処理については、こちの記事の「Google IDでログインするときの処理」を参照してください。
そして、現在ログインしているGoogle IDで再ログインすると、Google IDでユーザー登録したユーザーの削除が行われます。
// delUser.js抜粋
~
const newUserEmail = newUser.email;
await deleteUser(newUser);
~
メールアドレスとパスワードでユーザー登録したユーザーと同じように、reauthenticateWithCredential()によって再認証しようとしたのですが、いろいろ調べてもGoogle IDで登録したユーザーでの再認証処理を作りこむことができませんでした。
ということで、ちょっとみっともないのですが、ログインのポップアップを再表示し、再ログオンすることによって、再認証させました。
最後に
Google IDでユーザー登録したユーザーの削除には苦労しました。
reauthenticateWithCredential()によって再認証ってできると思って、いろいろ調べて、いろいろテストしてみたんですが、出来ませんでした。
Google IDでユーザー登録したユーザーの削除の部分のコードが汚いのもそれが理由です。
もしも、このコードを参考するのなら、このあまりきれいでない部分は修正してください。
当該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
このブログに掲載されているコードは、著作権者(私)に帰属します。コードを自由に使用、複製、改変、配布、販売、サブライセンスできます。また、コードを使用した結果、何らかの損害が発生した場合でも、著作権者は一切責任を負いません。